Skip to content

Commit 0c2d3f7

Browse files
bradfitzrsc
authored andcommitted
net/http: on Transport body write error, wait briefly for a response
From #11745 (comment) : The http.RoundTripper interface says you get either a *Response, or an error. But in the case of a client writing a large request and the server replying prematurely (e.g. 403 Forbidden) and closing the connection without reading the request body, what does the client want? The 403 response, or the error that the body couldn't be copied? This CL implements the aforementioned comment's option c), making the Transport give an N millisecond advantage to responses over body write errors. Updates #11745 Change-Id: I4485a782505d54de6189f6856a7a1f33ce4d5e5e Reviewed-on: https://go-review.googlesource.com/12590 Reviewed-by: Russ Cox <[email protected]>
1 parent a60c536 commit 0c2d3f7

File tree

2 files changed

+82
-7
lines changed

2 files changed

+82
-7
lines changed

src/net/http/transport.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,6 +1164,19 @@ WaitResponse:
11641164
for {
11651165
select {
11661166
case err := <-writeErrCh:
1167+
if isSyscallWriteError(err) {
1168+
// Issue 11745. If we failed to write the request
1169+
// body, it's possible the server just heard enough
1170+
// and already wrote to us. Prioritize the server's
1171+
// response over returning a body write error.
1172+
select {
1173+
case re = <-resc:
1174+
pc.close()
1175+
break WaitResponse
1176+
case <-time.After(50 * time.Millisecond):
1177+
// Fall through.
1178+
}
1179+
}
11671180
if err != nil {
11681181
re = responseAndError{nil, err}
11691182
pc.close()
@@ -1366,3 +1379,16 @@ type fakeLocker struct{}
13661379

13671380
func (fakeLocker) Lock() {}
13681381
func (fakeLocker) Unlock() {}
1382+
1383+
func isSyscallWriteError(err error) bool {
1384+
switch e := err.(type) {
1385+
case *url.Error:
1386+
return isSyscallWriteError(e.Err)
1387+
case *net.OpError:
1388+
return e.Op == "write" && isSyscallWriteError(e.Err)
1389+
case *os.SyscallError:
1390+
return e.Syscall == "write"
1391+
default:
1392+
return false
1393+
}
1394+
}

src/net/http/transport_test.go

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import (
1818
"io/ioutil"
1919
"log"
2020
"net"
21-
"net/http"
2221
. "net/http"
2322
"net/http/httptest"
2423
"net/url"
@@ -320,7 +319,7 @@ func TestTransportReadToEndReusesConn(t *testing.T) {
320319
addrSeen[r.RemoteAddr]++
321320
if r.URL.Path == "/chunked/" {
322321
w.WriteHeader(200)
323-
w.(http.Flusher).Flush()
322+
w.(Flusher).Flush()
324323
} else {
325324
w.Header().Set("Content-Type", strconv.Itoa(len(msg)))
326325
w.WriteHeader(200)
@@ -335,7 +334,7 @@ func TestTransportReadToEndReusesConn(t *testing.T) {
335334
wantLen := []int{len(msg), -1}[pi]
336335
addrSeen = make(map[string]int)
337336
for i := 0; i < 3; i++ {
338-
res, err := http.Get(ts.URL + path)
337+
res, err := Get(ts.URL + path)
339338
if err != nil {
340339
t.Errorf("Get %s: %v", path, err)
341340
continue
@@ -1976,7 +1975,7 @@ func TestIdleConnChannelLeak(t *testing.T) {
19761975
// then closes it.
19771976
func TestTransportClosesRequestBody(t *testing.T) {
19781977
defer afterTest(t)
1979-
ts := httptest.NewServer(http.HandlerFunc(func(w ResponseWriter, r *Request) {
1978+
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
19801979
io.Copy(ioutil.Discard, r.Body)
19811980
}))
19821981
defer ts.Close()
@@ -2346,13 +2345,13 @@ func TestTransportDialTLS(t *testing.T) {
23462345
// Test for issue 8755
23472346
// Ensure that if a proxy returns an error, it is exposed by RoundTrip
23482347
func TestRoundTripReturnsProxyError(t *testing.T) {
2349-
badProxy := func(*http.Request) (*url.URL, error) {
2348+
badProxy := func(*Request) (*url.URL, error) {
23502349
return nil, errors.New("errorMessage")
23512350
}
23522351

23532352
tr := &Transport{Proxy: badProxy}
23542353

2355-
req, _ := http.NewRequest("GET", "http://example.com", nil)
2354+
req, _ := NewRequest("GET", "http://example.com", nil)
23562355

23572356
_, err := tr.RoundTrip(req)
23582357

@@ -2644,7 +2643,57 @@ func TestTransportFlushesBodyChunks(t *testing.T) {
26442643
}
26452644
}
26462645

2647-
func wantBody(res *http.Response, err error, want string) error {
2646+
// Issue 11745.
2647+
func TestTransportPrefersResponseOverWriteError(t *testing.T) {
2648+
if testing.Short() {
2649+
t.Skip("skipping in short mode")
2650+
}
2651+
defer afterTest(t)
2652+
const contentLengthLimit = 1024 * 1024 // 1MB
2653+
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
2654+
if r.ContentLength >= contentLengthLimit {
2655+
w.WriteHeader(StatusBadRequest)
2656+
r.Body.Close()
2657+
return
2658+
}
2659+
w.WriteHeader(StatusOK)
2660+
}))
2661+
defer ts.Close()
2662+
2663+
fail := 0
2664+
count := 100
2665+
bigBody := strings.Repeat("a", contentLengthLimit*2)
2666+
for i := 0; i < count; i++ {
2667+
req, err := NewRequest("PUT", ts.URL, strings.NewReader(bigBody))
2668+
if err != nil {
2669+
t.Fatal(err)
2670+
}
2671+
tr := new(Transport)
2672+
defer tr.CloseIdleConnections()
2673+
client := &Client{Transport: tr}
2674+
resp, err := client.Do(req)
2675+
if err != nil {
2676+
fail++
2677+
t.Logf("%d = %#v", i, err)
2678+
if ue, ok := err.(*url.Error); ok {
2679+
t.Logf("urlErr = %#v", ue.Err)
2680+
if ne, ok := ue.Err.(*net.OpError); ok {
2681+
t.Logf("netOpError = %#v", ne.Err)
2682+
}
2683+
}
2684+
} else {
2685+
resp.Body.Close()
2686+
if resp.StatusCode != 400 {
2687+
t.Errorf("Expected status code 400, got %v", resp.Status)
2688+
}
2689+
}
2690+
}
2691+
if fail > 0 {
2692+
t.Errorf("Failed %v out of %v\n", fail, count)
2693+
}
2694+
}
2695+
2696+
func wantBody(res *Response, err error, want string) error {
26482697
if err != nil {
26492698
return err
26502699
}

0 commit comments

Comments
 (0)