Skip to content

Commit eea8c88

Browse files
glasserbradfitz
authored andcommitted
net/http: make Transport retry GetBody requests if nothing written
This is another attempt at the change attempted in https://golang.org/cl/27117 and rolled back in https://golang.org/cl/34134 The difference between this and the previous attempt is that this version only retries if the new field GetBody is set on the Request. Additionally, this allows retries of requests with idempotent methods even if they have bodies, as long as GetBody is defined. This also fixes an existing bug where readLoop could make a redundant call to setReqCanceler for DELETE/POST/PUT/etc requests with no body with zero bytes written. This clarifies the existing TestRetryIdempotentRequestsOnError test (and changes it into a test with 4 subtests). When that test was written, it was in fact testing "retry idempotent requests" logic, but the logic had changed since then, and it was actually testing "retry requests with no body when no bytes have been written". (You can confirm this by changing the existing test from a GET to a DELETE; it passes without the changes in this CL.) We now test for the no-Body and GetBody cases for both idempotent and nothing-written-non-idempotent requests. Fixes #18241 Fixes #17844 Change-Id: I69a48691796f6dc08c31f7aa7887b7dfd67e278a Reviewed-on: https://go-review.googlesource.com/42142 Run-TryBot: Brad Fitzpatrick <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Brad Fitzpatrick <[email protected]>
1 parent c8ab8c1 commit eea8c88

File tree

6 files changed

+186
-82
lines changed

6 files changed

+186
-82
lines changed

src/net/http/client_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1727,8 +1727,8 @@ func (b issue18239Body) Close() error {
17271727
return nil
17281728
}
17291729

1730-
// Issue 18239: make sure the Transport doesn't retry requests with bodies.
1731-
// (Especially if Request.GetBody is not defined.)
1730+
// Issue 18239: make sure the Transport doesn't retry requests with bodies
1731+
// if Request.GetBody is not defined.
17321732
func TestTransportBodyReadError(t *testing.T) {
17331733
setParallel(t)
17341734
defer afterTest(t)

src/net/http/export_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ var (
2525
ExportCloseWriteAndWait = (*conn).closeWriteAndWait
2626
ExportErrRequestCanceled = errRequestCanceled
2727
ExportErrRequestCanceledConn = errRequestCanceledConn
28+
ExportErrServerClosedIdle = errServerClosedIdle
2829
ExportServeFile = serveFile
2930
ExportScanETag = scanETag
3031
ExportHttp2ConfigureServer = http2ConfigureServer

src/net/http/request.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1317,7 +1317,7 @@ func (r *Request) closeBody() {
13171317
}
13181318

13191319
func (r *Request) isReplayable() bool {
1320-
if r.Body == nil {
1320+
if r.Body == nil || r.Body == NoBody || r.GetBody != nil {
13211321
switch valueOrDefault(r.Method, "GET") {
13221322
case "GET", "HEAD", "OPTIONS", "TRACE":
13231323
return true

src/net/http/transport.go

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,18 @@ func (t *Transport) RoundTrip(req *Request) (*Response, error) {
419419
return nil, err
420420
}
421421
testHookRoundTripRetried()
422+
423+
// Rewind the body if we're able to. (HTTP/2 does this itself so we only
424+
// need to do it for HTTP/1.1 connections.)
425+
if req.GetBody != nil && pconn.alt == nil {
426+
newReq := *req
427+
var err error
428+
newReq.Body, err = req.GetBody()
429+
if err != nil {
430+
return nil, err
431+
}
432+
req = &newReq
433+
}
422434
}
423435
}
424436

@@ -450,8 +462,9 @@ func (pc *persistConn) shouldRetryRequest(req *Request, err error) bool {
450462
return false
451463
}
452464
if _, ok := err.(nothingWrittenError); ok {
453-
// We never wrote anything, so it's safe to retry.
454-
return true
465+
// We never wrote anything, so it's safe to retry, if there's no body or we
466+
// can "rewind" the body with GetBody.
467+
return req.outgoingLength() == 0 || req.GetBody != nil
455468
}
456469
if !req.isReplayable() {
457470
// Don't retry non-idempotent requests.
@@ -1475,7 +1488,7 @@ func (pc *persistConn) mapRoundTripError(req *transportRequest, startBytesWritte
14751488
}
14761489
if pc.isBroken() {
14771490
<-pc.writeLoopDone
1478-
if pc.nwrite == startBytesWritten && req.outgoingLength() == 0 {
1491+
if pc.nwrite == startBytesWritten {
14791492
return nothingWrittenError{err}
14801493
}
14811494
return fmt.Errorf("net/http: HTTP/1.x transport connection broken: %v", err)
@@ -1544,16 +1557,6 @@ func (pc *persistConn) readLoop() {
15441557
err = fmt.Errorf("net/http: server response headers exceeded %d bytes; aborted", pc.maxHeaderResponseSize())
15451558
}
15461559

1547-
// If we won't be able to retry this request later (from the
1548-
// roundTrip goroutine), mark it as done now.
1549-
// BEFORE the send on rc.ch, as the client might re-use the
1550-
// same *Request pointer, and we don't want to set call
1551-
// t.setReqCanceler from this persistConn while the Transport
1552-
// potentially spins up a different persistConn for the
1553-
// caller's subsequent request.
1554-
if !pc.shouldRetryRequest(rc.req, err) {
1555-
pc.t.setReqCanceler(rc.req, nil)
1556-
}
15571560
select {
15581561
case rc.ch <- responseAndError{err: err}:
15591562
case <-rc.callerGone:
@@ -1768,7 +1771,7 @@ func (pc *persistConn) writeLoop() {
17681771
}
17691772
if err != nil {
17701773
wr.req.Request.closeBody()
1771-
if pc.nwrite == startBytesWritten && wr.req.outgoingLength() == 0 {
1774+
if pc.nwrite == startBytesWritten {
17721775
err = nothingWrittenError{err}
17731776
}
17741777
}

src/net/http/transport_internal_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package http
99
import (
1010
"errors"
1111
"net"
12+
"strings"
1213
"testing"
1314
)
1415

@@ -81,6 +82,19 @@ func dummyRequest(method string) *Request {
8182
}
8283
return req
8384
}
85+
func dummyRequestWithBody(method string) *Request {
86+
req, err := NewRequest(method, "http://fake.tld/", strings.NewReader("foo"))
87+
if err != nil {
88+
panic(err)
89+
}
90+
return req
91+
}
92+
93+
func dummyRequestWithBodyNoGetBody(method string) *Request {
94+
req := dummyRequestWithBody(method)
95+
req.GetBody = nil
96+
return req
97+
}
8498

8599
func TestTransportShouldRetryRequest(t *testing.T) {
86100
tests := []struct {
@@ -132,6 +146,18 @@ func TestTransportShouldRetryRequest(t *testing.T) {
132146
err: errServerClosedIdle,
133147
want: true,
134148
},
149+
7: {
150+
pc: &persistConn{reused: true},
151+
req: dummyRequestWithBody("POST"),
152+
err: nothingWrittenError{},
153+
want: true,
154+
},
155+
8: {
156+
pc: &persistConn{reused: true},
157+
req: dummyRequestWithBodyNoGetBody("POST"),
158+
err: nothingWrittenError{},
159+
want: false,
160+
},
135161
}
136162
for i, tt := range tests {
137163
got := tt.pc.shouldRetryRequest(tt.req, tt.err)

src/net/http/transport_test.go

Lines changed: 139 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2601,86 +2601,160 @@ type writerFuncConn struct {
26012601

26022602
func (c writerFuncConn) Write(p []byte) (n int, err error) { return c.write(p) }
26032603

2604-
// Issue 4677. If we try to reuse a connection that the server is in the
2605-
// process of closing, we may end up successfully writing out our request (or a
2606-
// portion of our request) only to find a connection error when we try to read
2607-
// from (or finish writing to) the socket.
2604+
// Issues 4677, 18241, and 17844. If we try to reuse a connection that the
2605+
// server is in the process of closing, we may end up successfully writing out
2606+
// our request (or a portion of our request) only to find a connection error
2607+
// when we try to read from (or finish writing to) the socket.
26082608
//
2609-
// NOTE: we resend a request only if the request is idempotent, we reused a
2610-
// keep-alive connection, and we haven't yet received any header data. This
2611-
// automatically prevents an infinite resend loop because we'll run out of the
2612-
// cached keep-alive connections eventually.
2613-
func TestRetryIdempotentRequestsOnError(t *testing.T) {
2614-
defer afterTest(t)
2609+
// NOTE: we resend a request only if:
2610+
// - we reused a keep-alive connection
2611+
// - we haven't yet received any header data
2612+
// - either we wrote no bytes to the server, or the request is idempotent
2613+
// This automatically prevents an infinite resend loop because we'll run out of
2614+
// the cached keep-alive connections eventually.
2615+
func TestRetryRequestsOnError(t *testing.T) {
2616+
newRequest := func(method, urlStr string, body io.Reader) *Request {
2617+
req, err := NewRequest(method, urlStr, body)
2618+
if err != nil {
2619+
t.Fatal(err)
2620+
}
2621+
return req
2622+
}
26152623

2616-
var (
2617-
mu sync.Mutex
2618-
logbuf bytes.Buffer
2619-
)
2620-
logf := func(format string, args ...interface{}) {
2621-
mu.Lock()
2622-
defer mu.Unlock()
2623-
fmt.Fprintf(&logbuf, format, args...)
2624-
logbuf.WriteByte('\n')
2624+
testCases := []struct {
2625+
name string
2626+
failureN int
2627+
failureErr error
2628+
// Note that we can't just re-use the Request object across calls to c.Do
2629+
// because we need to rewind Body between calls. (GetBody is only used to
2630+
// rewind Body on failure and redirects, not just because it's done.)
2631+
req func() *Request
2632+
reqString string
2633+
}{
2634+
{
2635+
name: "IdempotentNoBodySomeWritten",
2636+
// Believe that we've written some bytes to the server, so we know we're
2637+
// not just in the "retry when no bytes sent" case".
2638+
failureN: 1,
2639+
// Use the specific error that shouldRetryRequest looks for with idempotent requests.
2640+
failureErr: ExportErrServerClosedIdle,
2641+
req: func() *Request {
2642+
return newRequest("GET", "http://fake.golang", nil)
2643+
},
2644+
reqString: `GET / HTTP/1.1\r\nHost: fake.golang\r\nUser-Agent: Go-http-client/1.1\r\nAccept-Encoding: gzip\r\n\r\n`,
2645+
},
2646+
{
2647+
name: "IdempotentGetBodySomeWritten",
2648+
// Believe that we've written some bytes to the server, so we know we're
2649+
// not just in the "retry when no bytes sent" case".
2650+
failureN: 1,
2651+
// Use the specific error that shouldRetryRequest looks for with idempotent requests.
2652+
failureErr: ExportErrServerClosedIdle,
2653+
req: func() *Request {
2654+
return newRequest("GET", "http://fake.golang", strings.NewReader("foo\n"))
2655+
},
2656+
reqString: `GET / HTTP/1.1\r\nHost: fake.golang\r\nUser-Agent: Go-http-client/1.1\r\nContent-Length: 4\r\nAccept-Encoding: gzip\r\n\r\nfoo\n`,
2657+
},
2658+
{
2659+
name: "NothingWrittenNoBody",
2660+
// It's key that we return 0 here -- that's what enables Transport to know
2661+
// that nothing was written, even though this is a non-idempotent request.
2662+
failureN: 0,
2663+
failureErr: errors.New("second write fails"),
2664+
req: func() *Request {
2665+
return newRequest("DELETE", "http://fake.golang", nil)
2666+
},
2667+
reqString: `DELETE / HTTP/1.1\r\nHost: fake.golang\r\nUser-Agent: Go-http-client/1.1\r\nAccept-Encoding: gzip\r\n\r\n`,
2668+
},
2669+
{
2670+
name: "NothingWrittenGetBody",
2671+
// It's key that we return 0 here -- that's what enables Transport to know
2672+
// that nothing was written, even though this is a non-idempotent request.
2673+
failureN: 0,
2674+
failureErr: errors.New("second write fails"),
2675+
// Note that NewRequest will set up GetBody for strings.Reader, which is
2676+
// required for the retry to occur
2677+
req: func() *Request {
2678+
return newRequest("POST", "http://fake.golang", strings.NewReader("foo\n"))
2679+
},
2680+
reqString: `POST / HTTP/1.1\r\nHost: fake.golang\r\nUser-Agent: Go-http-client/1.1\r\nContent-Length: 4\r\nAccept-Encoding: gzip\r\n\r\nfoo\n`,
2681+
},
26252682
}
26262683

2627-
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
2628-
logf("Handler")
2629-
w.Header().Set("X-Status", "ok")
2630-
}))
2631-
defer ts.Close()
2684+
for _, tc := range testCases {
2685+
t.Run(tc.name, func(t *testing.T) {
2686+
defer afterTest(t)
26322687

2633-
var writeNumAtomic int32
2634-
c := ts.Client()
2635-
c.Transport.(*Transport).Dial = func(network, addr string) (net.Conn, error) {
2636-
logf("Dial")
2637-
c, err := net.Dial(network, ts.Listener.Addr().String())
2638-
if err != nil {
2639-
logf("Dial error: %v", err)
2640-
return nil, err
2641-
}
2642-
return &writerFuncConn{
2643-
Conn: c,
2644-
write: func(p []byte) (n int, err error) {
2645-
if atomic.AddInt32(&writeNumAtomic, 1) == 2 {
2646-
logf("intentional write failure")
2647-
return 0, errors.New("second write fails")
2688+
var (
2689+
mu sync.Mutex
2690+
logbuf bytes.Buffer
2691+
)
2692+
logf := func(format string, args ...interface{}) {
2693+
mu.Lock()
2694+
defer mu.Unlock()
2695+
fmt.Fprintf(&logbuf, format, args...)
2696+
logbuf.WriteByte('\n')
2697+
}
2698+
2699+
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
2700+
logf("Handler")
2701+
w.Header().Set("X-Status", "ok")
2702+
}))
2703+
defer ts.Close()
2704+
2705+
var writeNumAtomic int32
2706+
c := ts.Client()
2707+
c.Transport.(*Transport).Dial = func(network, addr string) (net.Conn, error) {
2708+
logf("Dial")
2709+
c, err := net.Dial(network, ts.Listener.Addr().String())
2710+
if err != nil {
2711+
logf("Dial error: %v", err)
2712+
return nil, err
26482713
}
2649-
logf("Write(%q)", p)
2650-
return c.Write(p)
2651-
},
2652-
}, nil
2653-
}
2714+
return &writerFuncConn{
2715+
Conn: c,
2716+
write: func(p []byte) (n int, err error) {
2717+
if atomic.AddInt32(&writeNumAtomic, 1) == 2 {
2718+
logf("intentional write failure")
2719+
return tc.failureN, tc.failureErr
2720+
}
2721+
logf("Write(%q)", p)
2722+
return c.Write(p)
2723+
},
2724+
}, nil
2725+
}
26542726

2655-
SetRoundTripRetried(func() {
2656-
logf("Retried.")
2657-
})
2658-
defer SetRoundTripRetried(nil)
2727+
SetRoundTripRetried(func() {
2728+
logf("Retried.")
2729+
})
2730+
defer SetRoundTripRetried(nil)
26592731

2660-
for i := 0; i < 3; i++ {
2661-
res, err := c.Get("http://fake.golang/")
2662-
if err != nil {
2663-
t.Fatalf("i=%d: Get = %v", i, err)
2664-
}
2665-
res.Body.Close()
2666-
}
2732+
for i := 0; i < 3; i++ {
2733+
res, err := c.Do(tc.req())
2734+
if err != nil {
2735+
t.Fatalf("i=%d: Do = %v", i, err)
2736+
}
2737+
res.Body.Close()
2738+
}
26672739

2668-
mu.Lock()
2669-
got := logbuf.String()
2670-
mu.Unlock()
2671-
const want = `Dial
2672-
Write("GET / HTTP/1.1\r\nHost: fake.golang\r\nUser-Agent: Go-http-client/1.1\r\nAccept-Encoding: gzip\r\n\r\n")
2740+
mu.Lock()
2741+
got := logbuf.String()
2742+
mu.Unlock()
2743+
want := fmt.Sprintf(`Dial
2744+
Write("%s")
26732745
Handler
26742746
intentional write failure
26752747
Retried.
26762748
Dial
2677-
Write("GET / HTTP/1.1\r\nHost: fake.golang\r\nUser-Agent: Go-http-client/1.1\r\nAccept-Encoding: gzip\r\n\r\n")
2749+
Write("%s")
26782750
Handler
2679-
Write("GET / HTTP/1.1\r\nHost: fake.golang\r\nUser-Agent: Go-http-client/1.1\r\nAccept-Encoding: gzip\r\n\r\n")
2751+
Write("%s")
26802752
Handler
2681-
`
2682-
if got != want {
2683-
t.Errorf("Log of events differs. Got:\n%s\nWant:\n%s", got, want)
2753+
`, tc.reqString, tc.reqString, tc.reqString)
2754+
if got != want {
2755+
t.Errorf("Log of events differs. Got:\n%s\nWant:\n%s", got, want)
2756+
}
2757+
})
26842758
}
26852759
}
26862760

0 commit comments

Comments
 (0)