Commit eea8c88a authored by David Glasser's avatar David Glasser Committed by Brad Fitzpatrick

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 <bradfitz@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: default avatarBrad Fitzpatrick <bradfitz@golang.org>
parent c8ab8c1f
...@@ -1727,8 +1727,8 @@ func (b issue18239Body) Close() error { ...@@ -1727,8 +1727,8 @@ func (b issue18239Body) Close() error {
return nil return nil
} }
// Issue 18239: make sure the Transport doesn't retry requests with bodies. // Issue 18239: make sure the Transport doesn't retry requests with bodies
// (Especially if Request.GetBody is not defined.) // if Request.GetBody is not defined.
func TestTransportBodyReadError(t *testing.T) { func TestTransportBodyReadError(t *testing.T) {
setParallel(t) setParallel(t)
defer afterTest(t) defer afterTest(t)
......
...@@ -25,6 +25,7 @@ var ( ...@@ -25,6 +25,7 @@ var (
ExportCloseWriteAndWait = (*conn).closeWriteAndWait ExportCloseWriteAndWait = (*conn).closeWriteAndWait
ExportErrRequestCanceled = errRequestCanceled ExportErrRequestCanceled = errRequestCanceled
ExportErrRequestCanceledConn = errRequestCanceledConn ExportErrRequestCanceledConn = errRequestCanceledConn
ExportErrServerClosedIdle = errServerClosedIdle
ExportServeFile = serveFile ExportServeFile = serveFile
ExportScanETag = scanETag ExportScanETag = scanETag
ExportHttp2ConfigureServer = http2ConfigureServer ExportHttp2ConfigureServer = http2ConfigureServer
......
...@@ -1317,7 +1317,7 @@ func (r *Request) closeBody() { ...@@ -1317,7 +1317,7 @@ func (r *Request) closeBody() {
} }
func (r *Request) isReplayable() bool { func (r *Request) isReplayable() bool {
if r.Body == nil { if r.Body == nil || r.Body == NoBody || r.GetBody != nil {
switch valueOrDefault(r.Method, "GET") { switch valueOrDefault(r.Method, "GET") {
case "GET", "HEAD", "OPTIONS", "TRACE": case "GET", "HEAD", "OPTIONS", "TRACE":
return true return true
......
...@@ -419,6 +419,18 @@ func (t *Transport) RoundTrip(req *Request) (*Response, error) { ...@@ -419,6 +419,18 @@ func (t *Transport) RoundTrip(req *Request) (*Response, error) {
return nil, err return nil, err
} }
testHookRoundTripRetried() testHookRoundTripRetried()
// Rewind the body if we're able to. (HTTP/2 does this itself so we only
// need to do it for HTTP/1.1 connections.)
if req.GetBody != nil && pconn.alt == nil {
newReq := *req
var err error
newReq.Body, err = req.GetBody()
if err != nil {
return nil, err
}
req = &newReq
}
} }
} }
...@@ -450,8 +462,9 @@ func (pc *persistConn) shouldRetryRequest(req *Request, err error) bool { ...@@ -450,8 +462,9 @@ func (pc *persistConn) shouldRetryRequest(req *Request, err error) bool {
return false return false
} }
if _, ok := err.(nothingWrittenError); ok { if _, ok := err.(nothingWrittenError); ok {
// We never wrote anything, so it's safe to retry. // We never wrote anything, so it's safe to retry, if there's no body or we
return true // can "rewind" the body with GetBody.
return req.outgoingLength() == 0 || req.GetBody != nil
} }
if !req.isReplayable() { if !req.isReplayable() {
// Don't retry non-idempotent requests. // Don't retry non-idempotent requests.
...@@ -1475,7 +1488,7 @@ func (pc *persistConn) mapRoundTripError(req *transportRequest, startBytesWritte ...@@ -1475,7 +1488,7 @@ func (pc *persistConn) mapRoundTripError(req *transportRequest, startBytesWritte
} }
if pc.isBroken() { if pc.isBroken() {
<-pc.writeLoopDone <-pc.writeLoopDone
if pc.nwrite == startBytesWritten && req.outgoingLength() == 0 { if pc.nwrite == startBytesWritten {
return nothingWrittenError{err} return nothingWrittenError{err}
} }
return fmt.Errorf("net/http: HTTP/1.x transport connection broken: %v", err) return fmt.Errorf("net/http: HTTP/1.x transport connection broken: %v", err)
...@@ -1544,16 +1557,6 @@ func (pc *persistConn) readLoop() { ...@@ -1544,16 +1557,6 @@ func (pc *persistConn) readLoop() {
err = fmt.Errorf("net/http: server response headers exceeded %d bytes; aborted", pc.maxHeaderResponseSize()) err = fmt.Errorf("net/http: server response headers exceeded %d bytes; aborted", pc.maxHeaderResponseSize())
} }
// If we won't be able to retry this request later (from the
// roundTrip goroutine), mark it as done now.
// BEFORE the send on rc.ch, as the client might re-use the
// same *Request pointer, and we don't want to set call
// t.setReqCanceler from this persistConn while the Transport
// potentially spins up a different persistConn for the
// caller's subsequent request.
if !pc.shouldRetryRequest(rc.req, err) {
pc.t.setReqCanceler(rc.req, nil)
}
select { select {
case rc.ch <- responseAndError{err: err}: case rc.ch <- responseAndError{err: err}:
case <-rc.callerGone: case <-rc.callerGone:
...@@ -1768,7 +1771,7 @@ func (pc *persistConn) writeLoop() { ...@@ -1768,7 +1771,7 @@ func (pc *persistConn) writeLoop() {
} }
if err != nil { if err != nil {
wr.req.Request.closeBody() wr.req.Request.closeBody()
if pc.nwrite == startBytesWritten && wr.req.outgoingLength() == 0 { if pc.nwrite == startBytesWritten {
err = nothingWrittenError{err} err = nothingWrittenError{err}
} }
} }
......
...@@ -9,6 +9,7 @@ package http ...@@ -9,6 +9,7 @@ package http
import ( import (
"errors" "errors"
"net" "net"
"strings"
"testing" "testing"
) )
...@@ -81,6 +82,19 @@ func dummyRequest(method string) *Request { ...@@ -81,6 +82,19 @@ func dummyRequest(method string) *Request {
} }
return req return req
} }
func dummyRequestWithBody(method string) *Request {
req, err := NewRequest(method, "http://fake.tld/", strings.NewReader("foo"))
if err != nil {
panic(err)
}
return req
}
func dummyRequestWithBodyNoGetBody(method string) *Request {
req := dummyRequestWithBody(method)
req.GetBody = nil
return req
}
func TestTransportShouldRetryRequest(t *testing.T) { func TestTransportShouldRetryRequest(t *testing.T) {
tests := []struct { tests := []struct {
...@@ -132,6 +146,18 @@ func TestTransportShouldRetryRequest(t *testing.T) { ...@@ -132,6 +146,18 @@ func TestTransportShouldRetryRequest(t *testing.T) {
err: errServerClosedIdle, err: errServerClosedIdle,
want: true, want: true,
}, },
7: {
pc: &persistConn{reused: true},
req: dummyRequestWithBody("POST"),
err: nothingWrittenError{},
want: true,
},
8: {
pc: &persistConn{reused: true},
req: dummyRequestWithBodyNoGetBody("POST"),
err: nothingWrittenError{},
want: false,
},
} }
for i, tt := range tests { for i, tt := range tests {
got := tt.pc.shouldRetryRequest(tt.req, tt.err) got := tt.pc.shouldRetryRequest(tt.req, tt.err)
......
...@@ -2601,86 +2601,160 @@ type writerFuncConn struct { ...@@ -2601,86 +2601,160 @@ type writerFuncConn struct {
func (c writerFuncConn) Write(p []byte) (n int, err error) { return c.write(p) } func (c writerFuncConn) Write(p []byte) (n int, err error) { return c.write(p) }
// Issue 4677. If we try to reuse a connection that the server is in the // Issues 4677, 18241, and 17844. If we try to reuse a connection that the
// process of closing, we may end up successfully writing out our request (or a // server is in the process of closing, we may end up successfully writing out
// portion of our request) only to find a connection error when we try to read // our request (or a portion of our request) only to find a connection error
// from (or finish writing to) the socket. // when we try to read from (or finish writing to) the socket.
// //
// NOTE: we resend a request only if the request is idempotent, we reused a // NOTE: we resend a request only if:
// keep-alive connection, and we haven't yet received any header data. This // - we reused a keep-alive connection
// automatically prevents an infinite resend loop because we'll run out of the // - we haven't yet received any header data
// cached keep-alive connections eventually. // - either we wrote no bytes to the server, or the request is idempotent
func TestRetryIdempotentRequestsOnError(t *testing.T) { // This automatically prevents an infinite resend loop because we'll run out of
defer afterTest(t) // the cached keep-alive connections eventually.
func TestRetryRequestsOnError(t *testing.T) {
newRequest := func(method, urlStr string, body io.Reader) *Request {
req, err := NewRequest(method, urlStr, body)
if err != nil {
t.Fatal(err)
}
return req
}
var ( testCases := []struct {
mu sync.Mutex name string
logbuf bytes.Buffer failureN int
) failureErr error
logf := func(format string, args ...interface{}) { // Note that we can't just re-use the Request object across calls to c.Do
mu.Lock() // because we need to rewind Body between calls. (GetBody is only used to
defer mu.Unlock() // rewind Body on failure and redirects, not just because it's done.)
fmt.Fprintf(&logbuf, format, args...) req func() *Request
logbuf.WriteByte('\n') reqString string
}{
{
name: "IdempotentNoBodySomeWritten",
// Believe that we've written some bytes to the server, so we know we're
// not just in the "retry when no bytes sent" case".
failureN: 1,
// Use the specific error that shouldRetryRequest looks for with idempotent requests.
failureErr: ExportErrServerClosedIdle,
req: func() *Request {
return newRequest("GET", "http://fake.golang", nil)
},
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`,
},
{
name: "IdempotentGetBodySomeWritten",
// Believe that we've written some bytes to the server, so we know we're
// not just in the "retry when no bytes sent" case".
failureN: 1,
// Use the specific error that shouldRetryRequest looks for with idempotent requests.
failureErr: ExportErrServerClosedIdle,
req: func() *Request {
return newRequest("GET", "http://fake.golang", strings.NewReader("foo\n"))
},
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`,
},
{
name: "NothingWrittenNoBody",
// It's key that we return 0 here -- that's what enables Transport to know
// that nothing was written, even though this is a non-idempotent request.
failureN: 0,
failureErr: errors.New("second write fails"),
req: func() *Request {
return newRequest("DELETE", "http://fake.golang", nil)
},
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`,
},
{
name: "NothingWrittenGetBody",
// It's key that we return 0 here -- that's what enables Transport to know
// that nothing was written, even though this is a non-idempotent request.
failureN: 0,
failureErr: errors.New("second write fails"),
// Note that NewRequest will set up GetBody for strings.Reader, which is
// required for the retry to occur
req: func() *Request {
return newRequest("POST", "http://fake.golang", strings.NewReader("foo\n"))
},
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`,
},
} }
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { for _, tc := range testCases {
logf("Handler") t.Run(tc.name, func(t *testing.T) {
w.Header().Set("X-Status", "ok") defer afterTest(t)
}))
defer ts.Close()
var writeNumAtomic int32 var (
c := ts.Client() mu sync.Mutex
c.Transport.(*Transport).Dial = func(network, addr string) (net.Conn, error) { logbuf bytes.Buffer
logf("Dial") )
c, err := net.Dial(network, ts.Listener.Addr().String()) logf := func(format string, args ...interface{}) {
if err != nil { mu.Lock()
logf("Dial error: %v", err) defer mu.Unlock()
return nil, err fmt.Fprintf(&logbuf, format, args...)
} logbuf.WriteByte('\n')
return &writerFuncConn{ }
Conn: c,
write: func(p []byte) (n int, err error) { ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
if atomic.AddInt32(&writeNumAtomic, 1) == 2 { logf("Handler")
logf("intentional write failure") w.Header().Set("X-Status", "ok")
return 0, errors.New("second write fails") }))
defer ts.Close()
var writeNumAtomic int32
c := ts.Client()
c.Transport.(*Transport).Dial = func(network, addr string) (net.Conn, error) {
logf("Dial")
c, err := net.Dial(network, ts.Listener.Addr().String())
if err != nil {
logf("Dial error: %v", err)
return nil, err
} }
logf("Write(%q)", p) return &writerFuncConn{
return c.Write(p) Conn: c,
}, write: func(p []byte) (n int, err error) {
}, nil if atomic.AddInt32(&writeNumAtomic, 1) == 2 {
} logf("intentional write failure")
return tc.failureN, tc.failureErr
}
logf("Write(%q)", p)
return c.Write(p)
},
}, nil
}
SetRoundTripRetried(func() { SetRoundTripRetried(func() {
logf("Retried.") logf("Retried.")
}) })
defer SetRoundTripRetried(nil) defer SetRoundTripRetried(nil)
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
res, err := c.Get("http://fake.golang/") res, err := c.Do(tc.req())
if err != nil { if err != nil {
t.Fatalf("i=%d: Get = %v", i, err) t.Fatalf("i=%d: Do = %v", i, err)
} }
res.Body.Close() res.Body.Close()
} }
mu.Lock() mu.Lock()
got := logbuf.String() got := logbuf.String()
mu.Unlock() mu.Unlock()
const want = `Dial want := fmt.Sprintf(`Dial
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") Write("%s")
Handler Handler
intentional write failure intentional write failure
Retried. Retried.
Dial Dial
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") Write("%s")
Handler Handler
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") Write("%s")
Handler Handler
` `, tc.reqString, tc.reqString, tc.reqString)
if got != want { if got != want {
t.Errorf("Log of events differs. Got:\n%s\nWant:\n%s", got, want) t.Errorf("Log of events differs. Got:\n%s\nWant:\n%s", got, want)
}
})
} }
} }
......
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