0

I have an HTTP client with a custom RoundTripper which in turn uses the http.DefaultTransport to handle the request. Now imagine I have a slow server which takes a long time to respond and it makes my http client timeout and cancel the client. Here is the code for the client:

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "time"
)

type rt struct {
    roundTripper func(req *http.Request) (*http.Response, error)
}

func (r rt) RoundTrip(req *http.Request) (*http.Response, error) {
    return r.roundTripper(req)
}

func main() {

    c := http.Client{
        Timeout:   3 * time.Second,
        Transport:  rt{RoundTripper(http.DefaultTransport)},
    }
    resp, err := c.Get("http://127.0.0.1:9000")
    if err != nil {
        fmt.Println("err:", err)
    } else {
        body, err := ioutil.ReadAll(resp.Body)
        resp.Body.Close()
        fmt.Println(string(body), err)
    }

}
func RoundTripper(next http.RoundTripper) func(req *http.Request) (*http.Response, error) {
    return func(req *http.Request) (*http.Response, error) {
        resp, err := next.RoundTrip(req)
        if err != nil {
            return nil, fmt.Errorf("err: %w", err)
        }
        return resp, nil
    }
}

The problem here is that the error I'm receiving on timeout is randomly one of net/http: request canceled or context deadline exceeded.
Now I know they should be semantically the same thing but I'm failing to understand why it's returning each and when?

Here is the server code if you want to try it for yourself.

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
2hamed
  • 8,719
  • 13
  • 69
  • 112
  • Can you shed light on what you try to do with all this roundtripping? – Volker Feb 24 '21 at 09:39
  • @Volker The actual `RoundTripper` in my code is for circuit-breaking, retrying, and some other domain-specific tasks. But it's all irrelevant. This happens inside the `http.Client`. – 2hamed Feb 24 '21 at 09:47
  • I don't see how you can be getting `context deadline exceeded`, since you aren't passing a context to your request. Is this your actual code? – Jonathan Hall Feb 24 '21 at 10:32
  • 1
    @Flimzy Yes, this is. You can try to run this exact code and see for yourself. – 2hamed Feb 24 '21 at 10:34
  • @Flimzy it may happen because selects are not deterministic and the request context is manipulated here `net/http/client.setRequestCancel(req *Request, rt RoundTripper, deadline time.Time) (stopTimer func(), didTimeout func() bool)` and the deadline argument you see comes from the `client.Timeout` field, which is used as a base when the timeout of the context is more than that one. – damianopetrungaro Feb 24 '21 at 10:57

1 Answers1

2

The function net/http/client.setRequestCancel() is used to set the cancel of the request. There are three ways

  • The second will return: net/http: request canceled
  • The third will return: context deadline exceeded

Because both use the same deadline, time.now()+client.Timeout. So according to the runtime schedule, the request will be cancelled randomly through these two methods.

    https://github.com/golang/go/blob/master/src/net/http/transport.go#L2652

    case <-cancelChan:
        // return err: net/http: request
        pc.t.CancelRequest(req.Request) canceled
        cancelChan = nil
    case <-ctxDoneChan:
        // return err:
        pc.t.cancelRequest(req.Request, req.Context().Err())
        cancelChan = nil
        ctxDoneChan = nil
nercoeus
  • 21
  • 3