Browse Source

http2: add automatic gzip compression for the Transport

Tests are in net/http (clientserver_test.go, TestH12_AutoGzip)
from https://golang.org/cl/17241

Fixes golang/go#13298

Change-Id: I3f0b237ffdf6d547d57f29383e1a78c4f272fc44
Reviewed-on: https://go-review.googlesource.com/17242
Reviewed-by: Andrew Gerrand <adg@golang.org>
Brad Fitzpatrick 10 years ago
parent
commit
b092070472
1 changed files with 79 additions and 7 deletions
  1. 79 7
      http2/transport.go

+ 79 - 7
http2/transport.go

@@ -9,6 +9,7 @@ package http2
 import (
 import (
 	"bufio"
 	"bufio"
 	"bytes"
 	"bytes"
+	"compress/gzip"
 	"crypto/tls"
 	"crypto/tls"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
@@ -61,10 +62,29 @@ type Transport struct {
 	// If nil, the default is used.
 	// If nil, the default is used.
 	ConnPool ClientConnPool
 	ConnPool ClientConnPool
 
 
+	// DisableCompression, if true, prevents the Transport from
+	// requesting compression with an "Accept-Encoding: gzip"
+	// request header when the Request contains no existing
+	// Accept-Encoding value. If the Transport requests gzip on
+	// its own and gets a gzipped response, it's transparently
+	// decoded in the Response.Body. However, if the user
+	// explicitly requested gzip it is not automatically
+	// uncompressed.
+	DisableCompression bool
+
 	connPoolOnce  sync.Once
 	connPoolOnce  sync.Once
 	connPoolOrDef ClientConnPool // non-nil version of ConnPool
 	connPoolOrDef ClientConnPool // non-nil version of ConnPool
 }
 }
 
 
+func (t *Transport) disableCompression() bool {
+	if t.DisableCompression {
+		return true
+	}
+	// TODO: also disable if this transport is somehow linked to an http1 Transport
+	// and it's configured there?
+	return false
+}
+
 var errTransportVersion = errors.New("http2: ConfigureTransport is only supported starting at Go 1.6")
 var errTransportVersion = errors.New("http2: ConfigureTransport is only supported starting at Go 1.6")
 
 
 // ConfigureTransport configures a net/http HTTP/1 Transport to use HTTP/2.
 // ConfigureTransport configures a net/http HTTP/1 Transport to use HTTP/2.
@@ -124,11 +144,12 @@ type ClientConn struct {
 // clientStream is the state for a single HTTP/2 stream. One of these
 // clientStream is the state for a single HTTP/2 stream. One of these
 // is created for each Transport.RoundTrip call.
 // is created for each Transport.RoundTrip call.
 type clientStream struct {
 type clientStream struct {
-	cc      *ClientConn
-	req     *http.Request
-	ID      uint32
-	resc    chan resAndError
-	bufPipe pipe // buffered pipe with the flow-controlled response payload
+	cc            *ClientConn
+	req           *http.Request
+	ID            uint32
+	resc          chan resAndError
+	bufPipe       pipe // buffered pipe with the flow-controlled response payload
+	requestedGzip bool
 
 
 	flow        flow  // guarded by cc.mu
 	flow        flow  // guarded by cc.mu
 	inflow      flow  // guarded by cc.mu
 	inflow      flow  // guarded by cc.mu
@@ -441,8 +462,28 @@ func (cc *ClientConn) RoundTrip(req *http.Request) (*http.Response, error) {
 	cs.req = req
 	cs.req = req
 	hasBody := req.Body != nil
 	hasBody := req.Body != nil
 
 
+	// TODO(bradfitz): this is a copy of the logic in net/http. Unify somewhere?
+	if !cc.t.disableCompression() &&
+		req.Header.Get("Accept-Encoding") == "" &&
+		req.Header.Get("Range") == "" &&
+		req.Method != "HEAD" {
+		// Request gzip only, not deflate. Deflate is ambiguous and
+		// not as universally supported anyway.
+		// See: http://www.gzip.org/zlib/zlib_faq.html#faq38
+		//
+		// Note that we don't request this for HEAD requests,
+		// due to a bug in nginx:
+		//   http://trac.nginx.org/nginx/ticket/358
+		//   https://golang.org/issue/5522
+		//
+		// We don't request gzip if the request is for a range, since
+		// auto-decoding a portion of a gzipped document will just fail
+		// anyway. See https://golang.org/issue/8923
+		cs.requestedGzip = true
+	}
+
 	// we send: HEADERS{1}, CONTINUATION{0,} + DATA{0,}
 	// we send: HEADERS{1}, CONTINUATION{0,} + DATA{0,}
-	hdrs := cc.encodeHeaders(req)
+	hdrs := cc.encodeHeaders(req, cs.requestedGzip)
 	first := true // first frame written (HEADERS is first, then CONTINUATION)
 	first := true // first frame written (HEADERS is first, then CONTINUATION)
 
 
 	cc.wmu.Lock()
 	cc.wmu.Lock()
@@ -598,7 +639,7 @@ func (cs *clientStream) awaitFlowControl(maxBytes int32) (taken int32, err error
 }
 }
 
 
 // requires cc.mu be held.
 // requires cc.mu be held.
-func (cc *ClientConn) encodeHeaders(req *http.Request) []byte {
+func (cc *ClientConn) encodeHeaders(req *http.Request, addGzipHeader bool) []byte {
 	cc.hbuf.Reset()
 	cc.hbuf.Reset()
 
 
 	// TODO(bradfitz): figure out :authority-vs-Host stuff between http2 and Go
 	// TODO(bradfitz): figure out :authority-vs-Host stuff between http2 and Go
@@ -626,6 +667,9 @@ func (cc *ClientConn) encodeHeaders(req *http.Request) []byte {
 			cc.writeHeader(lowKey, v)
 			cc.writeHeader(lowKey, v)
 		}
 		}
 	}
 	}
+	if addGzipHeader {
+		cc.writeHeader("accept-encoding", "gzip")
+	}
 	return cc.hbuf.Bytes()
 	return cc.hbuf.Bytes()
 }
 }
 
 
@@ -853,6 +897,13 @@ func (rl *clientConnReadLoop) processHeaderBlockFragment(frag []byte, streamID u
 		cs.bufPipe = pipe{b: buf}
 		cs.bufPipe = pipe{b: buf}
 		cs.bytesRemain = res.ContentLength
 		cs.bytesRemain = res.ContentLength
 		res.Body = transportResponseBody{cs}
 		res.Body = transportResponseBody{cs}
+
+		if cs.requestedGzip && res.Header.Get("Content-Encoding") == "gzip" {
+			res.Header.Del("Content-Encoding")
+			res.Header.Del("Content-Length")
+			res.ContentLength = -1
+			res.Body = &gzipReader{body: res.Body}
+		}
 	}
 	}
 
 
 	rl.activeRes[cs.ID] = cs
 	rl.activeRes[cs.ID] = cs
@@ -1146,3 +1197,24 @@ func strSliceContains(ss []string, s string) bool {
 type erringRoundTripper struct{ err error }
 type erringRoundTripper struct{ err error }
 
 
 func (rt erringRoundTripper) RoundTrip(*http.Request) (*http.Response, error) { return nil, rt.err }
 func (rt erringRoundTripper) RoundTrip(*http.Request) (*http.Response, error) { return nil, rt.err }
+
+// gzipReader wraps a response body so it can lazily
+// call gzip.NewReader on the first call to Read
+type gzipReader struct {
+	body io.ReadCloser // underlying Response.Body
+	zr   io.Reader     // lazily-initialized gzip reader
+}
+
+func (gz *gzipReader) Read(p []byte) (n int, err error) {
+	if gz.zr == nil {
+		gz.zr, err = gzip.NewReader(gz.body)
+		if err != nil {
+			return 0, err
+		}
+	}
+	return gz.zr.Read(p)
+}
+
+func (gz *gzipReader) Close() error {
+	return gz.body.Close()
+}