Search code examples
apigotwitterresponse

Reading response from Twitter client returns an error


I'm trying to make a function that calls twitter search API (using dghubble/go-twitter)

However, when I try to read the response from that API, i'm getting http2: response body closed

Here is my code:

func SearchTweets(query string) string {
    config := oauth1.NewConfig("consumer_key", "consumer_secret")
    token := oauth1.NewToken("access_token", "token_secret")
    httpClient := config.Client(oauth1.NoContext, token)

    // Twitter client
    client := twitter.NewClient(httpClient)

    // Search Tweets
    _ , resp, err := client.Search.Tweets(&twitter.SearchTweetParams{
        Query: "elon",
        Count: 50,
    })

    
    if err != nil {
        return err.Error()
    }

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return err.Error()
    }
    return string(body)
}

Solution

  • The line numbers and files referenced in this response were recorded on the date of answering and are not guaranteed to be eternally correct.

    From dghubble's go-twitter search.go, line 62, the client.Search.Tweets() function you are calling:

    resp, err := s.sling.New().Get("tweets.json").QueryStruct(params).Receive(search, apiError)
    

    Notice that it uses the sling package to construct and send the http request, as well as to receive and parse the http (JSON) response. The sling package to which it refers can be found here.

    Specifically, pay close attention to sling.go. On line 366, a comment states, "Receive is shorthand for calling Request and Do."

    The Request() function, found on line 280, simply constructs an *http.Request from the Sling object's properties. It returns said request, as well as an error (the signature is func (s *Sling) Request() (*http.Request, error))

    The Do() function is the critical part. It performs the http request passed as the first argument, attempts to parse the (JSON) response into the second argument provided (an interface{} which should point to the object you want the response to be parsed into), and simultaneously attempts to parse any errors into the third argument provided. After all of that, it closes the response body. Specifically, notice lines 385 and 386:

    // when err is nil, resp contains a non-nil resp.Body which must be closed
    defer resp.Body.Close()
    

    In other words, the response body is closed before it even makes it to the go-twitter Tweets() scope, so you're certainly not going to be able to read the body on your end. One point of these libraries, after all, is to save you the hastle of having to read and parse response bodies and the like.

    In general, you should be able to get the information you need using the packages provided. Specifically, the Tweets() function has the following signature: func (s *SearchService) Tweets(params *SearchTweetParams) (*Search, *http.Response, error). Notice that the first return value is of type *Search. This is the object read from Receive(), and in turn the Do() function. It should hold all of the information from the *http.Response's body before it was closed:

    search , resp, err := client.Search.Tweets(&twitter.SearchTweetParams{
        Query: "elon",
        Count: 50,
    })
    

    The search object is of type *Search, whose definition can be found here. It includes a slice of statuses (Tweet structures) as well as some metadata. You may also find the definition of a Tweet structure helpful, found here.

    Lastly, if for some reason you really need to be able to handle the request body yourself, you will have to execute the request from scratch, which mostly defeats the purpose of the go-twitter and sling libraries anyways. You can build off of sling somewhat to construct the request, but it's not pretty (and in fact, it'd probably be cleaner to construct the request from scratch too). Nevertheless, here is an (untested) example, code written by referencing the links previously mentioned in this post, as well as this one:

    func MyTweetsFunc(httpClient *http.Client, requestBuilder *sling.Sling, params *twitter.SearchTweetParams) 
    {
        // Use sling's Request() to construct the request, just as Receive() does
        req, err := requestBuilder.New().Get("tweets.json").QueryStruct(params).Request()
        if err != nil {
            // handle the error
        }
        
        // Get the response through the httpClient, since a sling client would parse and close the body automatically.
        resp, err := httpClient.Do(req)
        
        if err != nil {
            // handle the error
        }
    
        // Now you can do whatever you want with resp, which is simply an *http.Response. Its body is open for reading.
    }
    

    And the requestBuilder can be constructed like so:

    const twitterAPI = "https://api.twitter.com/1.1/"
    
    requestBuilder := sling.New().Client(httpClient).Base(twitterAPI)
    

    Where httpClient is the client you ordinarily pass into twitter.NewClient()