Skip to content

Commit 4643489

Browse files
committed
net/http: retry idempotent HTTP reqs on dead reused conns
If we try to reuse a connection that the server is in the process of closing, we may end up successfully writing out our request (or a portion of our request) only to find a connection error when we try to read from (or finish writing to) the socket. This manifests as an EOF returned from the Transport's RoundTrip. The issue, among others, is described in golang#4677. This change follows some of the Chromium guidelines for retrying idempotent requests only when the connection has been already been used successfully and no header data has yet been received for the response. Change-Id: I1ca630b944f0ed7ec1d3d46056a50fb959481a16
1 parent 19c1b16 commit 4643489

File tree

2 files changed

+131
-12
lines changed

2 files changed

+131
-12
lines changed

src/net/http/transport.go

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -221,18 +221,48 @@ func (t *Transport) RoundTrip(req *Request) (resp *Response, err error) {
221221
return nil, err
222222
}
223223

224-
// Get the cached or newly-created connection to either the
225-
// host (for http or https), the http proxy, or the http proxy
226-
// pre-CONNECTed to https server. In any case, we'll be ready
227-
// to send it requests.
228-
pconn, err := t.getConn(req, cm)
229-
if err != nil {
230-
t.setReqCanceler(req, nil)
231-
req.closeBody()
232-
return nil, err
233-
}
224+
var pconn *persistConn
225+
for {
226+
// Get the cached or newly-created connection to either the
227+
// host (for http or https), the http proxy, or the http proxy
228+
// pre-CONNECTed to https server. In any case, we'll be ready
229+
// to send it requests.
230+
pconn, err = t.getConn(req, cm)
231+
if err != nil {
232+
t.setReqCanceler(req, nil)
233+
req.closeBody()
234+
return nil, err
235+
}
234236

235-
return pconn.roundTrip(treq)
237+
resp, err = pconn.roundTrip(treq)
238+
if err != nil {
239+
if brhErr, ok := err.(beforeRespHeaderError); ok {
240+
err = brhErr.error // unwrap the custom error in case we return it
241+
if pconn.isReused() && (req.Method == "GET" || req.Method == "HEAD") {
242+
// If we try to reuse a connection that the server is in the process of
243+
// closing, we may end up successfully writing out our request (or a
244+
// portion of our request) only to find a connection error when we try to
245+
// read from (or finish writing to) the socket.
246+
247+
// There can be a race between the socket pool checking checking whether a
248+
// socket is still connected, receiving the FIN, and sending/reading data
249+
// on a reused socket. If we receive the FIN between the connectedness
250+
// check and writing/reading from the socket, we may first learn the socket
251+
// is disconnected when we get a ERR_SOCKET_NOT_CONNECTED. This will most
252+
// likely happen when trying to retrieve its IP address.
253+
// See http://crbug.com/105824 for more details.
254+
255+
// We resend a request only if we reused a keep-alive connection and
256+
// did not yet receive any header data. This automatically prevents an
257+
// infinite resend loop because we'll run out of the cached keep-alive
258+
// connections eventually.
259+
continue
260+
}
261+
}
262+
}
263+
break
264+
}
265+
return resp, err
236266
}
237267

238268
// RegisterProtocol registers a new protocol with scheme.
@@ -805,6 +835,7 @@ type persistConn struct {
805835
numExpectedResponses int
806836
closed bool // whether conn has been closed
807837
broken bool // an error has happened on this connection; marked broken so it's not reused.
838+
reused bool // whether conn has had successful request/response and is being reused.
808839
// mutateHeaderFunc is an optional func to modify extra
809840
// headers on each outbound request before it's written. (the
810841
// original Request given to RoundTrip is not modified)
@@ -819,6 +850,22 @@ func (pc *persistConn) isBroken() bool {
819850
return b
820851
}
821852

853+
// isReused reports whether this connection is in a known broken state.
854+
func (pc *persistConn) isReused() bool {
855+
pc.lk.Lock()
856+
r := pc.reused
857+
pc.lk.Unlock()
858+
return r
859+
}
860+
861+
// markReused marks this connection as having been successfully used for a request and response.
862+
func (pc *persistConn) markReused() bool {
863+
pc.lk.Lock()
864+
pc.reused = true
865+
pc.lk.Unlock()
866+
return true
867+
}
868+
822869
func (pc *persistConn) cancelRequest() {
823870
pc.conn.Close()
824871
}
@@ -840,6 +887,9 @@ func (pc *persistConn) readLoop() {
840887

841888
for alive {
842889
pb, err := pc.br.Peek(1)
890+
if err != nil {
891+
err = beforeRespHeaderError{err}
892+
}
843893

844894
pc.lk.Lock()
845895
if pc.numExpectedResponses == 0 {
@@ -910,13 +960,15 @@ func (pc *persistConn) readLoop() {
910960
err == nil &&
911961
!pc.sawEOF &&
912962
pc.wroteRequest() &&
963+
pc.markReused() &&
913964
pc.t.putIdleConn(pc)
914965
}
915966
}
916967

917968
if alive && !hasBody {
918969
alive = !pc.sawEOF &&
919970
pc.wroteRequest() &&
971+
pc.markReused() &&
920972
pc.t.putIdleConn(pc)
921973
}
922974

@@ -1028,6 +1080,12 @@ func (e *httpError) Temporary() bool { return true }
10281080
var errTimeout error = &httpError{err: "net/http: timeout awaiting response headers", timeout: true}
10291081
var errClosed error = &httpError{err: "net/http: transport closed before response was received"}
10301082

1083+
// beforeRespHeaderError is used to indicate when an IO error has occurred before
1084+
// any header data was received.
1085+
type beforeRespHeaderError struct {
1086+
error
1087+
}
1088+
10311089
func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {
10321090
pc.t.setReqCanceler(req.Request, pc.cancelRequest)
10331091
pc.lk.Lock()
@@ -1086,7 +1144,7 @@ WaitResponse:
10861144
select {
10871145
case err := <-writeErrCh:
10881146
if err != nil {
1089-
re = responseAndError{nil, err}
1147+
re = responseAndError{nil, beforeRespHeaderError{err}}
10901148
pc.close()
10911149
break WaitResponse
10921150
}

src/net/http/transport_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2096,6 +2096,67 @@ type closerFunc func() error
20962096

20972097
func (f closerFunc) Close() error { return f() }
20982098

2099+
// Issue 4677. If we try to reuse a connection that the server is in the
2100+
// process of closing, we may end up successfully writing out our request (or a
2101+
// portion of our request) only to find a connection error when we try to read
2102+
// from (or finish writing to) the socket.
2103+
//
2104+
// NOTE: we resend a request only if the request is idempotent, we reused a
2105+
// keep-alive connection, and we haven't yet received any header data. This
2106+
// automatically prevents an infinite resend loop because we'll run out of the
2107+
// cached keep-alive connections eventually.
2108+
func TestRetryIdempotentRequestsOnError(t *testing.T) {
2109+
defer afterTest(t)
2110+
2111+
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
2112+
}))
2113+
defer ts.Close()
2114+
2115+
tr := &Transport{}
2116+
c := &Client{Transport: tr}
2117+
var (
2118+
mu sync.Mutex // guards t
2119+
wg sync.WaitGroup
2120+
)
2121+
2122+
fatal := func(err error) {
2123+
mu.Lock()
2124+
defer mu.Unlock()
2125+
t.Fatal(err)
2126+
}
2127+
2128+
// open 2 conns
2129+
wg.Add(2)
2130+
for i := 0; i < 2; i++ {
2131+
go func() {
2132+
defer wg.Done()
2133+
res, err := c.Get(ts.URL)
2134+
if err != nil {
2135+
fatal(err)
2136+
}
2137+
res.Body.Close()
2138+
}()
2139+
}
2140+
wg.Wait()
2141+
2142+
wg.Add(2)
2143+
waitc := make(chan struct{})
2144+
for i := 0; i < 2; i++ {
2145+
go func() {
2146+
defer wg.Done()
2147+
<-waitc
2148+
res, err := c.Get(ts.URL)
2149+
if err != nil {
2150+
fatal(err)
2151+
}
2152+
res.Body.Close()
2153+
}()
2154+
}
2155+
ts.CloseClientConnections()
2156+
close(waitc)
2157+
wg.Wait()
2158+
}
2159+
20992160
// Issue 6981
21002161
func TestTransportClosesBodyOnError(t *testing.T) {
21012162
if runtime.GOOS == "plan9" {

0 commit comments

Comments
 (0)