Golang shorts #1 — Cancel http.Request using Context

I made a massive research in this field of http.Request, because I wanted to create a CPU efficient Rest API which getting resources from other APIs. The Golang from version 1.7 supporting the context in the http.Request so when I build an API, I can use the context in every request to my endpoint. It is good, because I know when the user stop the request before it finished, and I can handle it in my code. Most of the request spend too much time to call the different third-party APIs and getting the resources from them. So firstly I designed a concurrency model for that.

I created an endpoint domain.com/resources which purpose getting all the resources from the third-party APIs, for example from 20 different API. There is an incoming request:

func (s *Serv) Resources(w http.ResponseWriter, r *http.Request) {
// Timeout in context
context.WithTimeout(
r.Context(),
time.Duration(s.Timeout)*time.Millisecond
)
resService := resources.New(r.Context(), s.Log)
resService.Get([]byte({DATA}))
w.Write([]byte("resources"))
}

I added a basic timeout for the whole request, just to make sure it’s not running out of the time, because we have a 100 ms time frame for each of the requests. It is also calling the resources service Get method, which is handle the third-party APIs get functions.

func (s *Serv) Get(data []byte) (Response, error) {
var resp = Response{}
var errChan = make(chan error)
  rd := RequestData{}
err := json.Unmarshal(data, &rd)
if err != nil {
return resp, err
}
  getIPChan := s.generateGetIP(rd, errChan)
  for {
select {
case err := <-errChan:
if err != nil {
// HTTP error log it
break
}
case getIPResponse := <-getIPChan:
print(string(getIPResponse))
case <-s.Context.Done():
log.Fatal(s.Context.Err())
break
}
}
  return resp, nil
}

The Get method currently handle only one third-party API just to make it simple. The s.generateXXXX methods are following the generator concurrency patterns, and create a channel for each API. The select handle all of the generated channels and the error from everywhere an error occur, also handle the context Done which means the context Deadline/Timeout reached, or the request canceled.

The next section define the generator function:

func (s *Serv) generateGetIP(
data RequestData,
errChan chan error
) chan []byte {
req, _ := http.NewRequest("GET", "http://first-api.com", nil)
respChan := make(chan []byte)
  go httpRequest(s.Context, s.Client, req, respChan, errChan)
  return respChan
}

The method create a response channel and return it as the generated channel, also send it to the goroutine.

The httpRequest helper function is dealing with the http request, and the cancelation of the actual request.

func httpRequest(
ctx context.Context,
client *http.Client,
req *http.Request,
respChan chan []byte,
errChan chan error
) {
req = req.WithContext(ctx)
tr := &http.Transport{}
client.Transport = tr
  go func() {
resp, err := client.Do(req)
if err != nil {
errChan <- err
}
if resp != nil {
defer resp.Body.Close()
      respData, err := ioutil.ReadAll(resp.Body)
if err != nil {
errChan <- err
}
respChan <- respData
} else {
errChan <- errors.New("HTTP request failed")
}
}()
  for {
select {
case <-ctx.Done():
tr.CancelRequest(req)
errChan <- errors.New("HTTP request cancelled")
return
case <-errChan:
tr.CancelRequest(req)
return
}
}
}

The httpRequest firstly adding the actual context for the request to handle the cancelation. After create an http.Transport and attach it to the http.Client, it helps the request to be cancelable, because the tr.CancelRequest means the request will be terminated. It runs the http request in a new goroutine, just because after that the for-select section checking errors and if something happen and the context. If the context cancelled, it’s also terminate the http request as well.

Why is it too complex?

If the API service handle many of the incoming request it’s sends many of outgoing request to collect the resources from third-party APIs. I didn’t want to handle them anymore, when the actual incoming request is terminated. It saves a lots of resource.

Like what you read? Give PumpkinSeed a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.