io.ReadAll(response.Body) returns context cancelled error

821 Views Asked by At

I try to read from response.Body after request's context was cancelled and get context cancelled error, but only sometimes and only when response body is long

I am making a request with contextWithTimeout and cancel context before I have read body. My code looks like this:

type Client struct {
    httpClient *http.Client
}

func (c *Client) Req(ctx context.Context, timeout time.Duration, method string, url *url.URL, body io.Reader) (*http.Response, error) {
        ctx, cancel := context.WithTimeout(ctx, timeout)
    defer cancel()

    req, err := http.NewRequestWithContext(ctx, method, url.String(), body)
    if err != nil {
        return nil, err
    }

    resp, err := c.httpClient.Do(req)

    if err != nil {
        return nil, err
    }

    return resp, nil
}

then I call client.Req in another function and try to do io.ReadAll(response.Body), but when the response is long it sometimes returns context cancelled error. Why does it happen and why it does not happen with short responses?

2

There are 2 best solutions below

0
Brits On

When you run resp, err := c.httpClient.Do(req) the following happens (simplifying quite a bit - see here if you want details):

  1. Send the request
  2. Receive the response headers
  3. Return control to caller

You now call cancel which signals that the request should be cancelled. This is a signal, not an immediate action; the call to cancel effectively just closes a channel and returns. This means that the io.ReadAll(response.Body) you call after Req returns will run concurrently with the routines actioning the cancellation.

So you have a race between the response being read and the cancellation signal being acted upon. You may think that the signal must be acted upon before more data comes over the network, but there are a range of complicating factors:

  • The use of a buffered reader.
  • TCP packets (the entire response may be in one packet).
  • OS network buffers
  • Cancelling the request involves communication between a few go routines (until message gets here).

So in simple terms "it does not happen with short responses" because the response is read before the cancellation is acted upon. The larger the response, the more time it will take to read, so the more likely it is that the cancellation will take effect.

0
sasklacz On

Just call the defer cancel AFTER you've read the body. This article explains it in details too https://groups.google.com/g/golang-nuts/c/2FKwG6oEvos