Canceling requests in Go using context

Shaaz Ahmed
The Software Firehose
3 min readMar 23, 2018

--

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.Response
responses, 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 a select block.
  • using context.WithTimeout() , attaching that to the request with request.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

Different stages of request cancellation using timeouts(from this article)

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.

--

--

Shaaz Ahmed
The Software Firehose

Every reader should ask himself periodically “Toward what end, toward what end?” — but do not ask it too often lest you pass up the fun of programming. — Perlis