Skip to content

Commit eaef1ab

Browse files
committed
net/http: allow reuse connection if entire unread body is in buffer, v2
1 parent 861c90c commit eaef1ab

File tree

3 files changed

+74
-9
lines changed

3 files changed

+74
-9
lines changed

src/net/http/client_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,7 @@ func testClientInsecureTransport(t *testing.T, mode testMode) {
845845
if res != nil {
846846
res.Body.Close()
847847
}
848+
c.CloseIdleConnections()
848849
}
849850

850851
cst.close()

src/net/http/transport.go

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2359,13 +2359,27 @@ func (pc *persistConn) readLoop() {
23592359
}
23602360

23612361
waitForBodyRead := make(chan bool, 2)
2362+
23622363
body := &bodyEOFSignal{
23632364
body: resp.Body,
2364-
earlyCloseFn: func() error {
2365-
waitForBodyRead <- false
2365+
earlyCloseFn: func(r io.ReadCloser) error {
2366+
isEOF := false
2367+
if b, ok := r.(*body); ok {
2368+
if lr, ok := b.src.(*io.LimitedReader); ok {
2369+
if br, ok := (lr.R).(*bufio.Reader); ok {
2370+
if lr.N == int64(br.Buffered()) {
2371+
// if bufio.Reader buffer have all bytes remaining in LimitReader,
2372+
// discard the buffer then reuse connection, set EOF flag.
2373+
b.sawEOF = true
2374+
br.Discard(br.Buffered())
2375+
isEOF = true
2376+
}
2377+
}
2378+
}
2379+
}
2380+
waitForBodyRead <- isEOF
23662381
<-eofc // will be closed by deferred call at the end of the function
23672382
return nil
2368-
23692383
},
23702384
fn: func(err error) error {
23712385
isEOF := err == io.EOF
@@ -2981,11 +2995,11 @@ func canonicalAddr(url *url.URL) string {
29812995
// the return value from Close.
29822996
type bodyEOFSignal struct {
29832997
body io.ReadCloser
2984-
mu sync.Mutex // guards following 4 fields
2985-
closed bool // whether Close has been called
2986-
rerr error // sticky Read error
2987-
fn func(error) error // err will be nil on Read io.EOF
2988-
earlyCloseFn func() error // optional alt Close func used if io.EOF not seen
2998+
mu sync.Mutex // guards following 4 fields
2999+
closed bool // whether Close has been called
3000+
rerr error // sticky Read error
3001+
fn func(error) error // err will be nil on Read io.EOF
3002+
earlyCloseFn func(io.ReadCloser) error // optional alt Close func used if io.EOF not seen
29893003
}
29903004

29913005
var errReadOnClosedResBody = errors.New("http: read on closed response body")
@@ -3022,7 +3036,7 @@ func (es *bodyEOFSignal) Close() error {
30223036
}
30233037
es.closed = true
30243038
if es.earlyCloseFn != nil && es.rerr != io.EOF {
3025-
return es.earlyCloseFn()
3039+
return es.earlyCloseFn(es.body)
30263040
}
30273041
err := es.body.Close()
30283042
return es.condfn(err)

src/net/http/transport_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,56 @@ func testTransportReadToEndReusesConn(t *testing.T, mode testMode) {
474474
}
475475
}
476476

477+
// Tests that the HTTP transport re-uses connections when a client
478+
// early closes a response Body but the content is fully read into the underlying
479+
// buffer. So we can discard the body buffer and reuse the connection.
480+
func TestTransportReusesEarlyCloseButAllReceivedConn(t *testing.T) {
481+
run(t, testTransportReusesEarlyCloseButAllReceivedConn)
482+
}
483+
func testTransportReusesEarlyCloseButAllReceivedConn(t *testing.T, mode testMode) {
484+
const msg = "foobar"
485+
486+
var addrSeen map[string]int
487+
ts := newClientServerTest(t, mode, HandlerFunc(func(w ResponseWriter, r *Request) {
488+
addrSeen[r.RemoteAddr]++
489+
w.Header().Set("Content-Length", strconv.Itoa(len(msg)))
490+
w.WriteHeader(200)
491+
w.Write([]byte(msg))
492+
})).ts
493+
494+
wantLen := len(msg)
495+
addrSeen = make(map[string]int)
496+
total := 5
497+
for i := 0; i < total; i++ {
498+
res, err := ts.Client().Get(ts.URL)
499+
if err != nil {
500+
t.Errorf("Get /: %v", err)
501+
continue
502+
}
503+
504+
if res.ContentLength != int64(wantLen) {
505+
t.Errorf("res.ContentLength = %d; want %d", res.ContentLength, wantLen)
506+
}
507+
508+
if i+1 < total {
509+
// Close body directly. The body is small enough, so probably the underlying bufio.Reader
510+
// has read entire body into buffer. Thus even if the body is not read, the buffer is discarded
511+
// then connection is reused.
512+
res.Body.Close()
513+
} else {
514+
// when reading body, everything should be same.
515+
got, err := io.ReadAll(res.Body)
516+
if string(got) != msg || err != nil {
517+
t.Errorf("ReadAll(Body) = %q, %v; want %q, nil", string(got), err, msg)
518+
}
519+
}
520+
}
521+
522+
if len(addrSeen) != 1 {
523+
t.Errorf("server saw %d distinct client addresses; want 1", len(addrSeen))
524+
}
525+
}
526+
477527
func TestTransportMaxPerHostIdleConns(t *testing.T) {
478528
run(t, testTransportMaxPerHostIdleConns, []testMode{http1Mode})
479529
}

0 commit comments

Comments
 (0)