Skip to content

Commit b686e5f

Browse files
committed
internal/http3: add gzip support to transport
HTTP/3 now behaves similarly to HTTP/1 and HTTP/2 in terms of gzip support. Implementation of gzip reader and its pool are taken from net/http. For golang/go#70914 Change-Id: I829c0b9472e4c698b0f1a4012d8659b46a6a6964 Reviewed-on: https://go-review.googlesource.com/c/net/+/785120 LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Nicholas Husin <husin@google.com> Reviewed-by: Damien Neil <dneil@google.com>
1 parent 8a34885 commit b686e5f

6 files changed

Lines changed: 393 additions & 3 deletions

File tree

internal/http3/body.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,8 @@ func (r *bodyReader) Close() error {
236236
r.st.stream.CloseRead()
237237
// Make sure that any data that has already been written to bodyReader
238238
// cannot be read after it has been closed.
239+
r.mu.Lock()
240+
defer r.mu.Unlock()
239241
r.err = net.ErrClosed
240242
r.remain = 0
241243
return nil

internal/http3/gzip.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// Copyright 2026 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
package http3
5+
6+
import (
7+
"compress/flate"
8+
"compress/gzip"
9+
"errors"
10+
"io"
11+
"io/fs"
12+
"sync"
13+
)
14+
15+
var errConcurrentReadOnResBody = errors.New("http3: concurrent read on response body")
16+
17+
// gzipReader wraps a response body so it can lazily
18+
// get gzip.Reader from the pool on the first call to Read.
19+
// After Close is called it puts gzip.Reader to the pool immediately
20+
// if there is no Read in progress or later when Read completes.
21+
type gzipReader struct {
22+
body io.ReadCloser // underlying Response.Body
23+
mu sync.Mutex // guards zr and zerr
24+
zr *gzip.Reader // stores gzip reader from the pool between reads
25+
zerr error // sticky gzip reader init error or sentinel value to detect concurrent read and read after close
26+
}
27+
28+
type eofReader struct{}
29+
30+
func (eofReader) Read([]byte) (int, error) { return 0, io.EOF }
31+
func (eofReader) ReadByte() (byte, error) { return 0, io.EOF }
32+
33+
var gzipPool = sync.Pool{New: func() any { return new(gzip.Reader) }}
34+
35+
// gzipPoolGet gets a gzip.Reader from the pool and resets it to read from r.
36+
func gzipPoolGet(r io.Reader) (*gzip.Reader, error) {
37+
zr := gzipPool.Get().(*gzip.Reader)
38+
if err := zr.Reset(r); err != nil {
39+
gzipPoolPut(zr)
40+
return nil, err
41+
}
42+
return zr, nil
43+
}
44+
45+
// gzipPoolPut puts a gzip.Reader back into the pool.
46+
func gzipPoolPut(zr *gzip.Reader) {
47+
// Reset will allocate bufio.Reader if we pass it anything
48+
// other than a flate.Reader, so ensure that it's getting one.
49+
var r flate.Reader = eofReader{}
50+
zr.Reset(r)
51+
gzipPool.Put(zr)
52+
}
53+
54+
// acquire returns a gzip.Reader for reading response body.
55+
// The reader must be released after use.
56+
func (gz *gzipReader) acquire() (*gzip.Reader, error) {
57+
gz.mu.Lock()
58+
defer gz.mu.Unlock()
59+
if gz.zerr != nil {
60+
return nil, gz.zerr
61+
}
62+
if gz.zr == nil {
63+
// gzipPoolGet might block indefinitely since it reads the gzip header.
64+
// Therefore, drop mu temporarily when using gzipPoolGet.
65+
// We set zerr to errConcurrentReadOnResBody to prevent concurrent read
66+
// even when mu is temporarily dropped.
67+
gz.zerr = errConcurrentReadOnResBody
68+
gz.mu.Unlock()
69+
zr, err := gzipPoolGet(gz.body)
70+
gz.mu.Lock()
71+
// Guard against Close being called while gzipPoolGet is running.
72+
if gz.zerr != errConcurrentReadOnResBody {
73+
if zr != nil {
74+
gzipPoolPut(zr)
75+
}
76+
return nil, gz.zerr
77+
}
78+
gz.zr, gz.zerr = zr, err
79+
if gz.zerr != nil {
80+
return nil, gz.zerr
81+
}
82+
}
83+
ret := gz.zr
84+
gz.zr, gz.zerr = nil, errConcurrentReadOnResBody
85+
return ret, nil
86+
}
87+
88+
// release returns the gzip.Reader to the pool if Close was called during Read.
89+
func (gz *gzipReader) release(zr *gzip.Reader) {
90+
gz.mu.Lock()
91+
defer gz.mu.Unlock()
92+
if gz.zerr == errConcurrentReadOnResBody {
93+
gz.zr, gz.zerr = zr, nil
94+
} else { // fs.ErrClosed
95+
gzipPoolPut(zr)
96+
}
97+
}
98+
99+
// close returns the gzip.Reader to the pool immediately or
100+
// signals release to do so after Read completes.
101+
func (gz *gzipReader) close() {
102+
gz.mu.Lock()
103+
defer gz.mu.Unlock()
104+
if gz.zerr == nil && gz.zr != nil {
105+
gzipPoolPut(gz.zr)
106+
gz.zr = nil
107+
}
108+
gz.zerr = fs.ErrClosed
109+
}
110+
111+
func (gz *gzipReader) Read(p []byte) (n int, err error) {
112+
zr, err := gz.acquire()
113+
if err != nil {
114+
return 0, err
115+
}
116+
defer gz.release(zr)
117+
118+
return zr.Read(p)
119+
}
120+
121+
func (gz *gzipReader) Close() error {
122+
gz.close()
123+
124+
return gz.body.Close()
125+
}

internal/http3/roundtrip.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"net/http/httptrace"
1212
"net/textproto"
1313
"strconv"
14+
"strings"
1415
"sync"
1516

1617
"golang.org/x/net/http/httpguts"
@@ -129,6 +130,7 @@ func (cc *clientConn) RoundTrip(req *http.Request) (_ *http.Response, err error)
129130
st.stream.SetReadContext(req.Context())
130131
st.stream.SetWriteContext(req.Context())
131132

133+
addedGzip := httpcommon.IsRequestGzip(req.Method, req.Header, cc.tr.tr1.DisableCompression)
132134
headers := cc.enc.encode(func(yield func(itype indexType, name, value string)) {
133135
_, err = httpcommon.EncodeHeaders(req.Context(), httpcommon.EncodeHeadersParam{
134136
Request: httpcommon.Request{
@@ -139,7 +141,7 @@ func (cc *clientConn) RoundTrip(req *http.Request) (_ *http.Response, err error)
139141
Trailer: req.Trailer,
140142
ActualContentLength: actualContentLength(req),
141143
},
142-
AddGzipHeader: false, // TODO: add when appropriate
144+
AddGzipHeader: addedGzip,
143145
PeerMaxHeaderListSize: 0,
144146
DefaultUserAgent: "Go-http-client/3",
145147
}, func(name, value string) {
@@ -232,7 +234,13 @@ func (cc *clientConn) RoundTrip(req *http.Request) (_ *http.Response, err error)
232234
Trailer: trailer,
233235
Body: (*transportResponseBody)(rt),
234236
}
235-
// TODO: Automatic Content-Type: gzip decoding.
237+
if addedGzip && strings.EqualFold(h.Get("Content-Encoding"), "gzip") {
238+
resp.Body = &gzipReader{body: resp.Body}
239+
h.Del("Content-Encoding")
240+
h.Del("Content-Length")
241+
resp.ContentLength = -1
242+
resp.Uncompressed = true
243+
}
236244
return resp, nil
237245
case frameTypePushPromise:
238246
if err := cc.handlePushPromise(st); err != nil {

0 commit comments

Comments
 (0)