Canceling requests in Go using context
Recently, we wrote a small client library that simplifies making parallel HTTP requests in Golang. One of the required behaviours was the cancellation of all requests that took more time than the specified collective request timeout. Here’s what the library’s API looks like:
timeout := 200 * time.Millisecond
bulkClient := NewBulkHTTPClient(timeout)requestOne := http.NewRequest(http.MethodGet, '')
requestTwo := http.NewRequest(http.MethodGet, '')bulkRequest := NewBulkHTTPRequest()
bulkRequest.Add(requestOne)
bulkRequest.Add(requestTwo) // requestTwo is *http.Responseresponses, errors := bulkClient.Do(bulkRequest)
// return signature is []*http.Response, []errors
To cancel the HTTP requests, we initially explored a couple of options for the cancellation signal:
- using a
time.After()
channel within our client library and listening to it. When we receive a timeout signal, we stop listening to the response channel by exiting aselect
block. - using
context.WithTimeout()
, attaching that to the request withrequest.WithContext()
and then listening to thectx.Done()
. When we receive a signal on the channel, we stop listening to the response channel.
We chose to go with context.WithTimeout()
approach since it is also used by net/http
‘s http.Client.Do()
method to terminate the request at different stages of the client connection process. We weren’t very impressed with some of the consequences of using context.Context
, but decided to stick with it for lack of a better and convenient alternative. There’s a good deal written about the downsides of context
(here and here).
Usage, and the scope of a request
Before we discuss some of the gotchas and subtleties around the usage context to cancel HTTP requests, let’s take a practical example of how we can use it.
Note that I’ve mentioned the ioutil.ReadAll()
step above — that’s because the context is actually relevant until response.Body
has been read. If the context timeout occurs between lines 11 and 14, readErr
will be non-nil and will contain a context canceled
error along with a correctly read responseByteArray
. I couldn’t find any documentation regarding this, except for this thread on the golang-nuts Google group which mentions that the scope of a request ends only when the response is read. This makes sense for streamed responses, but it escapes me how it’s a good idea for normal ones.
Stages of HTTP Requests
Request cancellation isn’t possible for in-flight requests. I’d recommend reading this article for understanding the different stages of an HTTP connection with respect to net/http
and at what stages cancellations can occur. We’ve only observed requests being cancelled before and during connection establishment, but not after a successful connection establishment. However, we didn’t explore this further as this was sufficient for our cases.
Deprecated Methods
There were a couple of other methods we came across for canceling requests, which we learnt were deprecated. This is briefly explained in the comments of net/http/client.go
on lines 282–285.
// As background, there are three ways to cancel a request:
// First was Transport.CancelRequest. (deprecated)
// Second was Request.Cancel (this mechanism).
// Third was Request.Context.