Commit 7fc2625e authored by Brad Fitzpatrick's avatar Brad Fitzpatrick

net/http: propagate Client.Timeout down into Request's context deadline

Fixes #31657

Change-Id: I85e9595d3ea30d410f1f4b787925a6879a72bdf2
Reviewed-on: https://go-review.googlesource.com/c/go/+/175857Reviewed-by: default avatarBenny Siegert <bsiegert@gmail.com>
Run-TryBot: Benny Siegert <bsiegert@gmail.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
parent caf45cde
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
package http package http
import ( import (
"context"
"crypto/tls" "crypto/tls"
"encoding/base64" "encoding/base64"
"errors" "errors"
...@@ -18,6 +19,7 @@ import ( ...@@ -18,6 +19,7 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"net/url" "net/url"
"reflect"
"sort" "sort"
"strings" "strings"
"sync" "sync"
...@@ -273,46 +275,95 @@ func send(ireq *Request, rt RoundTripper, deadline time.Time) (resp *Response, d ...@@ -273,46 +275,95 @@ func send(ireq *Request, rt RoundTripper, deadline time.Time) (resp *Response, d
return resp, nil, nil return resp, nil, nil
} }
// setRequestCancel sets the Cancel field of req, if deadline is // timeBeforeContextDeadline reports whether the non-zero Time t is
// non-zero. The RoundTripper's type is used to determine whether the legacy // before ctx's deadline, if any. If ctx does not have a deadline, it
// CancelRequest behavior should be used. // always reports true (the deadline is considered infinite).
func timeBeforeContextDeadline(t time.Time, ctx context.Context) bool {
d, ok := ctx.Deadline()
if !ok {
return true
}
return t.Before(d)
}
// knownRoundTripperImpl reports whether rt is a RoundTripper that's
// maintained by the Go team and known to implement the latest
// optional semantics (notably contexts).
func knownRoundTripperImpl(rt RoundTripper) bool {
switch rt.(type) {
case *Transport, *http2Transport:
return true
}
// There's a very minor chance of a false positive with this.
// Insted of detecting our golang.org/x/net/http2.Transport,
// it might detect a Transport type in a different http2
// package. But I know of none, and the only problem would be
// some temporarily leaked goroutines if the transport didn't
// support contexts. So this is a good enough heuristic:
if reflect.TypeOf(rt).String() == "*http2.Transport" {
return true
}
return false
}
// setRequestCancel sets req.Cancel and adds a deadline context to req
// if deadline is non-zero. The RoundTripper's type is used to
// determine whether the legacy CancelRequest behavior should be used.
// //
// As background, there are three ways to cancel a request: // As background, there are three ways to cancel a request:
// First was Transport.CancelRequest. (deprecated) // First was Transport.CancelRequest. (deprecated)
// Second was Request.Cancel (this mechanism). // Second was Request.Cancel.
// Third was Request.Context. // Third was Request.Context.
// This function populates the second and third, and uses the first if it really needs to.
func setRequestCancel(req *Request, rt RoundTripper, deadline time.Time) (stopTimer func(), didTimeout func() bool) { func setRequestCancel(req *Request, rt RoundTripper, deadline time.Time) (stopTimer func(), didTimeout func() bool) {
if deadline.IsZero() { if deadline.IsZero() {
return nop, alwaysFalse return nop, alwaysFalse
} }
knownTransport := knownRoundTripperImpl(rt)
oldCtx := req.Context()
if req.Cancel == nil && knownTransport {
// If they already had a Request.Context that's
// expiring sooner, do nothing:
if !timeBeforeContextDeadline(deadline, oldCtx) {
return nop, alwaysFalse
}
var cancelCtx func()
req.ctx, cancelCtx = context.WithDeadline(oldCtx, deadline)
return cancelCtx, func() bool { return time.Now().After(deadline) }
}
initialReqCancel := req.Cancel // the user's original Request.Cancel, if any initialReqCancel := req.Cancel // the user's original Request.Cancel, if any
var cancelCtx func()
if oldCtx := req.Context(); timeBeforeContextDeadline(deadline, oldCtx) {
req.ctx, cancelCtx = context.WithDeadline(oldCtx, deadline)
}
cancel := make(chan struct{}) cancel := make(chan struct{})
req.Cancel = cancel req.Cancel = cancel
doCancel := func() { doCancel := func() {
// The newer way (the second way in the func comment): // The second way in the func comment above:
close(cancel) close(cancel)
// The first way, used only for RoundTripper
// The legacy compatibility way, used only // implementations written before Go 1.5 or Go 1.6.
// for RoundTripper implementations written type canceler interface{ CancelRequest(*Request) }
// before Go 1.5 or Go 1.6. if v, ok := rt.(canceler); ok {
type canceler interface {
CancelRequest(*Request)
}
switch v := rt.(type) {
case *Transport, *http2Transport:
// Do nothing. The net/http package's transports
// support the new Request.Cancel channel
case canceler:
v.CancelRequest(req) v.CancelRequest(req)
} }
} }
stopTimerCh := make(chan struct{}) stopTimerCh := make(chan struct{})
var once sync.Once var once sync.Once
stopTimer = func() { once.Do(func() { close(stopTimerCh) }) } stopTimer = func() {
once.Do(func() {
close(stopTimerCh)
if cancelCtx != nil {
cancelCtx()
}
})
}
timer := time.NewTimer(time.Until(deadline)) timer := time.NewTimer(time.Until(deadline))
var timedOut atomicBool var timedOut atomicBool
...@@ -870,8 +921,7 @@ func (b *cancelTimerBody) Read(p []byte) (n int, err error) { ...@@ -870,8 +921,7 @@ func (b *cancelTimerBody) Read(p []byte) (n int, err error) {
} }
if b.reqDidTimeout() { if b.reqDidTimeout() {
err = &httpError{ err = &httpError{
// TODO: early in cycle: s/Client.Timeout exceeded/timeout or context cancellation/ err: err.Error() + " (Client.Timeout or context cancellation while reading body)",
err: err.Error() + " (Client.Timeout exceeded while reading body)",
timeout: true, timeout: true,
} }
} }
......
...@@ -1274,7 +1274,7 @@ func testClientTimeout(t *testing.T, h2 bool) { ...@@ -1274,7 +1274,7 @@ func testClientTimeout(t *testing.T, h2 bool) {
} else if !ne.Timeout() { } else if !ne.Timeout() {
t.Errorf("net.Error.Timeout = false; want true") t.Errorf("net.Error.Timeout = false; want true")
} }
if got := ne.Error(); !strings.Contains(got, "Client.Timeout exceeded") { if got := ne.Error(); !strings.Contains(got, "(Client.Timeout") {
t.Errorf("error string = %q; missing timeout substring", got) t.Errorf("error string = %q; missing timeout substring", got)
} }
case <-time.After(failTime): case <-time.After(failTime):
...@@ -1917,3 +1917,22 @@ func TestClientCloseIdleConnections(t *testing.T) { ...@@ -1917,3 +1917,22 @@ func TestClientCloseIdleConnections(t *testing.T) {
t.Error("not closed") t.Error("not closed")
} }
} }
func TestClientPropagatesTimeoutToContext(t *testing.T) {
errDial := errors.New("not actually dialing")
c := &Client{
Timeout: 5 * time.Second,
Transport: &Transport{
DialContext: func(ctx context.Context, netw, addr string) (net.Conn, error) {
deadline, ok := ctx.Deadline()
if !ok {
t.Error("no deadline")
} else {
t.Logf("deadline in %v", deadline.Sub(time.Now()).Round(time.Second/10))
}
return nil, errDial
},
},
}
c.Get("https://example.tld/")
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment