Skip to content

Commit 6fd2d2c

Browse files
committed
net/http: make Transport retry non-idempotent requests if no bytes written
If the server failed on us before we even tried to write any bytes, it's safe to retry the request on a new connection, regardless of the HTTP method/idempotence. Fixes #15723 Change-Id: I25360f82aac530d12d2b3eef02c43ced86e62906 Reviewed-on: https://go-review.googlesource.com/27117 Reviewed-by: Andrew Gerrand <[email protected]> Run-TryBot: Brad Fitzpatrick <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
1 parent fe27291 commit 6fd2d2c

File tree

2 files changed

+72
-9
lines changed

2 files changed

+72
-9
lines changed

src/net/http/transport.go

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -421,19 +421,15 @@ func (pc *persistConn) shouldRetryRequest(req *Request, err error) bool {
421421
// our request (as opposed to sending an error).
422422
return false
423423
}
424+
if _, ok := err.(nothingWrittenError); ok {
425+
// We never wrote anything, so it's safe to retry.
426+
return true
427+
}
424428
if !req.isReplayable() {
425429
// Don't retry non-idempotent requests.
426-
427-
// TODO: swap the nothingWrittenError and isReplayable checks,
428-
// putting the "if nothingWrittenError => return true" case
429-
// first, per golang.org/issue/15723
430430
return false
431431
}
432-
switch err.(type) {
433-
case nothingWrittenError:
434-
// We never wrote anything, so it's safe to retry.
435-
return true
436-
case transportReadFromServerError:
432+
if _, ok := err.(transportReadFromServerError); ok {
437433
// We got some non-EOF net.Conn.Read failure reading
438434
// the 1st response byte from the server.
439435
return true

src/net/http/transport_internal_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,70 @@ func newLocalListener(t *testing.T) net.Listener {
7272
}
7373
return ln
7474
}
75+
76+
func dummyRequest(method string) *Request {
77+
req, err := NewRequest(method, "http://fake.tld/", nil)
78+
if err != nil {
79+
panic(err)
80+
}
81+
return req
82+
}
83+
84+
func TestTransportShouldRetryRequest(t *testing.T) {
85+
tests := []struct {
86+
pc *persistConn
87+
req *Request
88+
89+
err error
90+
want bool
91+
}{
92+
0: {
93+
pc: &persistConn{reused: false},
94+
req: dummyRequest("POST"),
95+
err: nothingWrittenError{},
96+
want: false,
97+
},
98+
1: {
99+
pc: &persistConn{reused: true},
100+
req: dummyRequest("POST"),
101+
err: nothingWrittenError{},
102+
want: true,
103+
},
104+
2: {
105+
pc: &persistConn{reused: true},
106+
req: dummyRequest("POST"),
107+
err: http2ErrNoCachedConn,
108+
want: true,
109+
},
110+
3: {
111+
pc: &persistConn{reused: true},
112+
req: dummyRequest("POST"),
113+
err: errMissingHost,
114+
want: false,
115+
},
116+
4: {
117+
pc: &persistConn{reused: true},
118+
req: dummyRequest("POST"),
119+
err: transportReadFromServerError{},
120+
want: false,
121+
},
122+
5: {
123+
pc: &persistConn{reused: true},
124+
req: dummyRequest("GET"),
125+
err: transportReadFromServerError{},
126+
want: true,
127+
},
128+
6: {
129+
pc: &persistConn{reused: true},
130+
req: dummyRequest("GET"),
131+
err: errServerClosedIdle,
132+
want: true,
133+
},
134+
}
135+
for i, tt := range tests {
136+
got := tt.pc.shouldRetryRequest(tt.req, tt.err)
137+
if got != tt.want {
138+
t.Errorf("%d. shouldRetryRequest = %v; want %v", i, got, tt.want)
139+
}
140+
}
141+
}

0 commit comments

Comments
 (0)