Skip to content

Commit 9296d4e

Browse files
committed
net/http: don't retry Transport requests if they have a body
This rolls back https://golang.org/cl/27117 partly, softening it so it only retries POST/PUT/DELETE etc requests where there's no Body (nil or NoBody). This is a little useless, since most idempotent requests have a body (except maybe DELETE), but it's late in the Go 1.8 release cycle and I want to do the proper fix. The proper fix will look like what we did for http2 and only retrying the request if Request.GetBody is defined, and then creating a new request for the next attempt. See https://golang.org/cl/33971 for the http2 fix. Updates #15723 Fixes #18239 Updates #18241 Change-Id: I6ebaa1fd9b19b5ccb23c8d9e7b3b236e71cf57f3 Reviewed-on: https://go-review.googlesource.com/34134 Run-TryBot: Brad Fitzpatrick <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Tom Bergan <[email protected]>
1 parent 67b2927 commit 9296d4e

File tree

3 files changed

+83
-8
lines changed

3 files changed

+83
-8
lines changed

doc/go1.8.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1311,7 +1311,8 @@ <h3 id="minor_library_changes">Minor changes to the library</h3>
13111311

13121312
<li><!-- CL 27117 -->
13131313
The <code>Transport</code> will now retry non-idempotent
1314-
requests if no bytes were written before a network failure.
1314+
requests if no bytes were written before a network failure
1315+
and the request has no body.
13151316
</li>
13161317

13171318
<li><!-- CL 32481 -->

src/net/http/client_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"strconv"
2727
"strings"
2828
"sync"
29+
"sync/atomic"
2930
"testing"
3031
"time"
3132
)
@@ -1738,3 +1739,76 @@ func TestClientRedirectTypes(t *testing.T) {
17381739
res.Body.Close()
17391740
}
17401741
}
1742+
1743+
// issue18239Body is an io.ReadCloser for TestTransportBodyReadError.
1744+
// Its Read returns readErr and increments *readCalls atomically.
1745+
// Its Close returns nil and increments *closeCalls atomically.
1746+
type issue18239Body struct {
1747+
readCalls *int32
1748+
closeCalls *int32
1749+
readErr error
1750+
}
1751+
1752+
func (b issue18239Body) Read([]byte) (int, error) {
1753+
atomic.AddInt32(b.readCalls, 1)
1754+
return 0, b.readErr
1755+
}
1756+
1757+
func (b issue18239Body) Close() error {
1758+
atomic.AddInt32(b.closeCalls, 1)
1759+
return nil
1760+
}
1761+
1762+
// Issue 18239: make sure the Transport doesn't retry requests with bodies.
1763+
// (Especially if Request.GetBody is not defined.)
1764+
func TestTransportBodyReadError(t *testing.T) {
1765+
setParallel(t)
1766+
defer afterTest(t)
1767+
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
1768+
if r.URL.Path == "/ping" {
1769+
return
1770+
}
1771+
buf := make([]byte, 1)
1772+
n, err := r.Body.Read(buf)
1773+
w.Header().Set("X-Body-Read", fmt.Sprintf("%v, %v", n, err))
1774+
}))
1775+
defer ts.Close()
1776+
tr := &Transport{}
1777+
defer tr.CloseIdleConnections()
1778+
c := &Client{Transport: tr}
1779+
1780+
// Do one initial successful request to create an idle TCP connection
1781+
// for the subsequent request to reuse. (The Transport only retries
1782+
// requests on reused connections.)
1783+
res, err := c.Get(ts.URL + "/ping")
1784+
if err != nil {
1785+
t.Fatal(err)
1786+
}
1787+
res.Body.Close()
1788+
1789+
var readCallsAtomic int32
1790+
var closeCallsAtomic int32 // atomic
1791+
someErr := errors.New("some body read error")
1792+
body := issue18239Body{&readCallsAtomic, &closeCallsAtomic, someErr}
1793+
1794+
req, err := NewRequest("POST", ts.URL, body)
1795+
if err != nil {
1796+
t.Fatal(err)
1797+
}
1798+
_, err = tr.RoundTrip(req)
1799+
if err != someErr {
1800+
t.Errorf("Got error: %v; want Request.Body read error: %v", err, someErr)
1801+
}
1802+
1803+
// And verify that our Body wasn't used multiple times, which
1804+
// would indicate retries. (as it buggily was during part of
1805+
// Go 1.8's dev cycle)
1806+
readCalls := atomic.LoadInt32(&readCallsAtomic)
1807+
closeCalls := atomic.LoadInt32(&closeCallsAtomic)
1808+
if readCalls != 1 {
1809+
t.Errorf("read calls = %d; want 1", readCalls)
1810+
}
1811+
if closeCalls != 1 {
1812+
t.Errorf("close calls = %d; want 1", closeCalls)
1813+
}
1814+
}

src/net/http/transport.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1384,7 +1384,7 @@ func (pc *persistConn) closeConnIfStillIdle() {
13841384
//
13851385
// The startBytesWritten value should be the value of pc.nwrite before the roundTrip
13861386
// started writing the request.
1387-
func (pc *persistConn) mapRoundTripErrorFromReadLoop(startBytesWritten int64, err error) (out error) {
1387+
func (pc *persistConn) mapRoundTripErrorFromReadLoop(req *Request, startBytesWritten int64, err error) (out error) {
13881388
if err == nil {
13891389
return nil
13901390
}
@@ -1399,7 +1399,7 @@ func (pc *persistConn) mapRoundTripErrorFromReadLoop(startBytesWritten int64, er
13991399
}
14001400
if pc.isBroken() {
14011401
<-pc.writeLoopDone
1402-
if pc.nwrite == startBytesWritten {
1402+
if pc.nwrite == startBytesWritten && req.outgoingLength() == 0 {
14031403
return nothingWrittenError{err}
14041404
}
14051405
}
@@ -1410,7 +1410,7 @@ func (pc *persistConn) mapRoundTripErrorFromReadLoop(startBytesWritten int64, er
14101410
// up to Transport.RoundTrip method when persistConn.roundTrip sees
14111411
// its pc.closech channel close, indicating the persistConn is dead.
14121412
// (after closech is closed, pc.closed is valid).
1413-
func (pc *persistConn) mapRoundTripErrorAfterClosed(startBytesWritten int64) error {
1413+
func (pc *persistConn) mapRoundTripErrorAfterClosed(req *Request, startBytesWritten int64) error {
14141414
if err := pc.canceled(); err != nil {
14151415
return err
14161416
}
@@ -1428,7 +1428,7 @@ func (pc *persistConn) mapRoundTripErrorAfterClosed(startBytesWritten int64) err
14281428
// see if we actually managed to write anything. If not, we
14291429
// can retry the request.
14301430
<-pc.writeLoopDone
1431-
if pc.nwrite == startBytesWritten {
1431+
if pc.nwrite == startBytesWritten && req.outgoingLength() == 0 {
14321432
return nothingWrittenError{err}
14331433
}
14341434

@@ -1710,7 +1710,7 @@ func (pc *persistConn) writeLoop() {
17101710
}
17111711
if err != nil {
17121712
wr.req.Request.closeBody()
1713-
if pc.nwrite == startBytesWritten {
1713+
if pc.nwrite == startBytesWritten && wr.req.outgoingLength() == 0 {
17141714
err = nothingWrittenError{err}
17151715
}
17161716
}
@@ -1911,14 +1911,14 @@ WaitResponse:
19111911
respHeaderTimer = timer.C
19121912
}
19131913
case <-pc.closech:
1914-
re = responseAndError{err: pc.mapRoundTripErrorAfterClosed(startBytesWritten)}
1914+
re = responseAndError{err: pc.mapRoundTripErrorAfterClosed(req.Request, startBytesWritten)}
19151915
break WaitResponse
19161916
case <-respHeaderTimer:
19171917
pc.close(errTimeout)
19181918
re = responseAndError{err: errTimeout}
19191919
break WaitResponse
19201920
case re = <-resc:
1921-
re.err = pc.mapRoundTripErrorFromReadLoop(startBytesWritten, re.err)
1921+
re.err = pc.mapRoundTripErrorFromReadLoop(req.Request, startBytesWritten, re.err)
19221922
break WaitResponse
19231923
case <-cancelChan:
19241924
pc.t.CancelRequest(req.Request)

0 commit comments

Comments
 (0)