"net/url"
"strings"
"sync"
+ "sync/atomic"
"time"
)
return c.doFollowingRedirects(req, shouldRedirectGet)
}
+func alwaysFalse() bool { return false }
+
func (c *Client) doFollowingRedirects(ireq *Request, shouldRedirect func(int) bool) (resp *Response, err error) {
var base *url.URL
redirectChecker := c.CheckRedirect
req := ireq
var timer *time.Timer
+ var atomicWasCanceled int32 // atomic bool (1 or 0)
+ var wasCanceled = alwaysFalse
if c.Timeout > 0 {
+ wasCanceled = func() bool { return atomic.LoadInt32(&atomicWasCanceled) != 0 }
type canceler interface {
CancelRequest(*Request)
}
return nil, fmt.Errorf("net/http: Client Transport of type %T doesn't support CancelRequest; Timeout not supported", c.transport())
}
timer = time.AfterFunc(c.Timeout, func() {
+ atomic.StoreInt32(&atomicWasCanceled, 1)
reqmu.Lock()
defer reqmu.Unlock()
tr.CancelRequest(req)
urlStr = req.URL.String()
if resp, err = c.send(req); err != nil {
+ if wasCanceled() {
+ err = &httpError{
+ err: err.Error() + " (Client.Timeout exceeded while awaiting headers)",
+ timeout: true,
+ }
+ }
break
}
continue
}
if timer != nil {
- resp.Body = &cancelTimerBody{timer, resp.Body}
+ resp.Body = &cancelTimerBody{
+ t: timer,
+ rc: resp.Body,
+ reqWasCanceled: wasCanceled,
+ }
}
return resp, nil
}
return c.doFollowingRedirects(req, shouldRedirectGet)
}
+// cancelTimerBody is an io.ReadCloser that wraps rc with two features:
+// 1) on Read EOF or Close, the timer t is Stopped,
+// 2) On Read failure, if reqWasCanceled is true, the error is wrapped and
+// marked as net.Error that hit its timeout.
type cancelTimerBody struct {
- t *time.Timer
- rc io.ReadCloser
+ t *time.Timer
+ rc io.ReadCloser
+ reqWasCanceled func() bool
}
func (b *cancelTimerBody) Read(p []byte) (n int, err error) {
n, err = b.rc.Read(p)
if err == io.EOF {
b.t.Stop()
+ } else if err != nil && b.reqWasCanceled() {
+ return n, &httpError{
+ err: err.Error() + " (Client.Timeout exceeded while reading body)",
+ timeout: true,
+ }
}
return
}
select {
case err := <-errc:
if err == nil {
- t.Error("expected error from ReadAll")
+ t.Fatal("expected error from ReadAll")
+ }
+ ne, ok := err.(net.Error)
+ if !ok {
+ t.Errorf("error value from ReadAll was %T; expected some net.Error", err)
+ } else if !ne.Timeout() {
+ t.Errorf("net.Error.Timeout = false; want true")
+ }
+ if got := ne.Error(); !strings.Contains(got, "Client.Timeout exceeded") {
+ t.Errorf("error string = %q; missing timeout substring", got)
}
- // Expected error.
case <-time.After(failTime):
t.Errorf("timeout after %v waiting for timeout of %v", failTime, timeout)
}
}
+// Client.Timeout firing before getting to the body
+func TestClientTimeout_Headers(t *testing.T) {
+ if testing.Short() {
+ t.Skip("skipping in short mode")
+ }
+ defer afterTest(t)
+ donec := make(chan bool)
+ ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
+ <-donec
+ }))
+ defer ts.Close()
+ defer close(donec)
+
+ c := &Client{Timeout: 500 * time.Millisecond}
+
+ _, err := c.Get(ts.URL)
+ if err == nil {
+ t.Fatal("got response from Get; expected error")
+ }
+ ue, ok := err.(*url.Error)
+ if !ok {
+ t.Fatalf("Got error of type %T; want *url.Error", err)
+ }
+ ne, ok := ue.Err.(net.Error)
+ if !ok {
+ t.Fatalf("Got url.Error.Err of type %T; want some net.Error", err)
+ }
+ if !ne.Timeout() {
+ t.Error("net.Error.Timeout = false; want true")
+ }
+ if got := ne.Error(); !strings.Contains(got, "Client.Timeout exceeded") {
+ t.Errorf("error string = %q; missing timeout substring", got)
+ }
+}
+
func TestClientRedirectEatsBody(t *testing.T) {
defer afterTest(t)
saw := make(chan string, 2)