Skip to content

Commit 18072ad

Browse files
committed
net/http: reuse HTTP/1 Transport conns more for gzipped responses
Flip around the composition order of the http.Response.Body's gzip.Reader vs. the reader which keeps track of waiting to see the end of the HTTP/1 response framing (whether that's a Content-Length or HTTP/1.1 chunking). Previously: user -> http.Response.Body -> bodyEOFSignal -> gzipReader -> gzip.Reader -> bufio.Reader [ -> http/1.1 de-chunking reader ] optional -> http1 framing *body But because bodyEOFSignal was waiting to see an EOF from the underlying gzip.Reader before reusing the connection, and gzip.Reader (or more specifically: the flate.Reader) wasn't returning an early io.EOF with the final chunk, the bodyEOfSignal was never releasing the connection, because the EOF from the http1 framing was read by a party who didn't care about it yet: the helper bufio.Reader created to do byte-at-a-time reading in the flate.Reader. Flip the read composition around to: user -> http.Response.Body -> gzipReader -> gzip.Reader -> bufio.Reader -> bodyEOFSignal [ -> http/1.1 de-chunking reader ] optional -> http1 framing *body Now when gzip.Reader does its byte-at-a-time reading via the bufio.Reader, the bufio.Reader will do its big reads against the bodyEOFSignal reader instead, which will then see the underlying http1 framing EOF, and be able to reuse the connection. Updates google/go-github#317 Updates #14867 And related abandoned fix to flate.Reader: https://golang.org/cl/21290 Change-Id: I3729dfdffe832ad943b84f4734b0f59b0e834749 Reviewed-on: https://go-review.googlesource.com/21291 Reviewed-by: David Symonds <[email protected]> Run-TryBot: Brad Fitzpatrick <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
1 parent 7f067c8 commit 18072ad

File tree

2 files changed

+97
-34
lines changed

2 files changed

+97
-34
lines changed

src/net/http/transport.go

Lines changed: 50 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,25 +1147,32 @@ func (pc *persistConn) readLoop() {
11471147
continue
11481148
}
11491149

1150-
if rc.addedGzip {
1151-
maybeUngzipResponse(resp)
1152-
}
1153-
resp.Body = &bodyEOFSignal{body: resp.Body}
1154-
11551150
waitForBodyRead := make(chan bool, 2)
1156-
resp.Body.(*bodyEOFSignal).earlyCloseFn = func() error {
1157-
waitForBodyRead <- false
1158-
return nil
1151+
body := &bodyEOFSignal{
1152+
body: resp.Body,
1153+
earlyCloseFn: func() error {
1154+
waitForBodyRead <- false
1155+
return nil
1156+
1157+
},
1158+
fn: func(err error) error {
1159+
isEOF := err == io.EOF
1160+
waitForBodyRead <- isEOF
1161+
if isEOF {
1162+
<-eofc // see comment above eofc declaration
1163+
} else if err != nil && pc.isCanceled() {
1164+
return errRequestCanceled
1165+
}
1166+
return err
1167+
},
11591168
}
1160-
resp.Body.(*bodyEOFSignal).fn = func(err error) error {
1161-
isEOF := err == io.EOF
1162-
waitForBodyRead <- isEOF
1163-
if isEOF {
1164-
<-eofc // see comment above eofc declaration
1165-
} else if err != nil && pc.isCanceled() {
1166-
return errRequestCanceled
1167-
}
1168-
return err
1169+
1170+
resp.Body = body
1171+
if rc.addedGzip && resp.Header.Get("Content-Encoding") == "gzip" {
1172+
resp.Body = &gzipReader{body: body}
1173+
resp.Header.Del("Content-Encoding")
1174+
resp.Header.Del("Content-Length")
1175+
resp.ContentLength = -1
11691176
}
11701177

11711178
select {
@@ -1199,15 +1206,6 @@ func (pc *persistConn) readLoop() {
11991206
}
12001207
}
12011208

1202-
func maybeUngzipResponse(resp *Response) {
1203-
if resp.Header.Get("Content-Encoding") == "gzip" {
1204-
resp.Header.Del("Content-Encoding")
1205-
resp.Header.Del("Content-Length")
1206-
resp.ContentLength = -1
1207-
resp.Body = &gzipReader{body: resp.Body}
1208-
}
1209-
}
1210-
12111209
func (pc *persistConn) readLoopPeekFailLocked(peekErr error) {
12121210
if pc.closed != nil {
12131211
return
@@ -1580,7 +1578,11 @@ func canonicalAddr(url *url.URL) string {
15801578
return addr
15811579
}
15821580

1583-
// bodyEOFSignal wraps a ReadCloser but runs fn (if non-nil) at most
1581+
// bodyEOFSignal is used by the HTTP/1 transport when reading response
1582+
// bodies to make sure we see the end of a response body before
1583+
// proceeding and reading on the connection again.
1584+
//
1585+
// It wraps a ReadCloser but runs fn (if non-nil) at most
15841586
// once, right before its final (error-producing) Read or Close call
15851587
// returns. fn should return the new error to return from Read or Close.
15861588
//
@@ -1596,12 +1598,14 @@ type bodyEOFSignal struct {
15961598
earlyCloseFn func() error // optional alt Close func used if io.EOF not seen
15971599
}
15981600

1601+
var errReadOnClosedResBody = errors.New("http: read on closed response body")
1602+
15991603
func (es *bodyEOFSignal) Read(p []byte) (n int, err error) {
16001604
es.mu.Lock()
16011605
closed, rerr := es.closed, es.rerr
16021606
es.mu.Unlock()
16031607
if closed {
1604-
return 0, errors.New("http: read on closed response body")
1608+
return 0, errReadOnClosedResBody
16051609
}
16061610
if rerr != nil {
16071611
return 0, rerr
@@ -1646,16 +1650,29 @@ func (es *bodyEOFSignal) condfn(err error) error {
16461650
// gzipReader wraps a response body so it can lazily
16471651
// call gzip.NewReader on the first call to Read
16481652
type gzipReader struct {
1649-
body io.ReadCloser // underlying Response.Body
1650-
zr io.Reader // lazily-initialized gzip reader
1653+
body *bodyEOFSignal // underlying HTTP/1 response body framing
1654+
zr *gzip.Reader // lazily-initialized gzip reader
1655+
zerr error // any error from gzip.NewReader; sticky
16511656
}
16521657

16531658
func (gz *gzipReader) Read(p []byte) (n int, err error) {
16541659
if gz.zr == nil {
1655-
gz.zr, err = gzip.NewReader(gz.body)
1656-
if err != nil {
1657-
return 0, err
1660+
if gz.zerr == nil {
1661+
gz.zr, gz.zerr = gzip.NewReader(gz.body)
16581662
}
1663+
if gz.zerr != nil {
1664+
return 0, gz.zerr
1665+
}
1666+
}
1667+
1668+
gz.body.mu.Lock()
1669+
if gz.body.closed {
1670+
err = errReadOnClosedResBody
1671+
}
1672+
gz.body.mu.Unlock()
1673+
1674+
if err != nil {
1675+
return 0, err
16591676
}
16601677
return gz.zr.Read(p)
16611678
}

src/net/http/transport_test.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -923,7 +923,9 @@ func TestTransportGzipRecursive(t *testing.T) {
923923
}))
924924
defer ts.Close()
925925

926-
c := &Client{Transport: &Transport{}}
926+
tr := &Transport{}
927+
defer tr.CloseIdleConnections()
928+
c := &Client{Transport: tr}
927929
res, err := c.Get(ts.URL)
928930
if err != nil {
929931
t.Fatal(err)
@@ -3044,6 +3046,50 @@ func TestNoCrashReturningTransportAltConn(t *testing.T) {
30443046
<-handledPendingDial
30453047
}
30463048

3049+
func TestTransportReuseConnection_Gzip_Chunked(t *testing.T) {
3050+
testTransportReuseConnection_Gzip(t, true)
3051+
}
3052+
3053+
func TestTransportReuseConnection_Gzip_ContentLength(t *testing.T) {
3054+
testTransportReuseConnection_Gzip(t, false)
3055+
}
3056+
3057+
// Make sure we re-use underlying TCP connection for gzipped responses too.
3058+
func testTransportReuseConnection_Gzip(t *testing.T, chunked bool) {
3059+
defer afterTest(t)
3060+
addr := make(chan string, 2)
3061+
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
3062+
addr <- r.RemoteAddr
3063+
w.Header().Set("Content-Encoding", "gzip")
3064+
if chunked {
3065+
w.(Flusher).Flush()
3066+
}
3067+
w.Write(rgz) // arbitrary gzip response
3068+
}))
3069+
defer ts.Close()
3070+
3071+
tr := &Transport{}
3072+
defer tr.CloseIdleConnections()
3073+
c := &Client{Transport: tr}
3074+
for i := 0; i < 2; i++ {
3075+
res, err := c.Get(ts.URL)
3076+
if err != nil {
3077+
t.Fatal(err)
3078+
}
3079+
buf := make([]byte, len(rgz))
3080+
if n, err := io.ReadFull(res.Body, buf); err != nil {
3081+
t.Errorf("%d. ReadFull = %v, %v", i, n, err)
3082+
}
3083+
// Note: no res.Body.Close call. It should work without it,
3084+
// since the flate.Reader's internal buffering will hit EOF
3085+
// and that should be sufficient.
3086+
}
3087+
a1, a2 := <-addr, <-addr
3088+
if a1 != a2 {
3089+
t.Fatalf("didn't reuse connection")
3090+
}
3091+
}
3092+
30473093
var errFakeRoundTrip = errors.New("fake roundtrip")
30483094

30493095
type funcRoundTripper func()

0 commit comments

Comments
 (0)