Skip to content

Commit

Permalink
Handle Accept-Ranges/Range headers correctly
Browse files Browse the repository at this point in the history
Fixes nytimes#83
Fixes #6
  • Loading branch information
CAFxX authored Jan 25, 2022
1 parent be1ad51 commit 5e6055a
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 0 deletions.
12 changes: 12 additions & 0 deletions adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
const (
vary = "Vary"
acceptEncoding = "Accept-Encoding"
acceptRanges = "Accept-Ranges"
contentEncoding = "Content-Encoding"
contentType = "Content-Type"
contentLength = "Content-Length"
Expand Down Expand Up @@ -76,6 +77,17 @@ func Adapter(opts ...Option) (func(http.Handler) http.Handler, error) {
return
}

// We do not handle range requests when compression is used, as the
// range specified applies to the compressed data, not to the uncompressed one.
// So we would need to (1) ensure that compressors are deterministic and (2)
// generate the whole uncompressed response anyway, compress it, and then discard
// the bits outside of the range.
// Let's keep it simple, and simply ignore completely the range header.
// We also need to remove the Accept: Range header from any response that is
// compressed; this is done in the ResponseWriter.
// See https://github.com/nytimes/gziphandler/issues/83.
r.Header.Del("Range")

gw, _ := writerPool.Get().(*compressWriter)
if gw == nil {
gw = &compressWriter{}
Expand Down
59 changes: 59 additions & 0 deletions adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,65 @@ func TestWriteStringEquivalence(t *testing.T) {
}
}

func TestAcceptRanges(t *testing.T) {
// Tests for https://github.com/nytimes/gziphandler/issues/83 https://github.com/CAFxX/httpcompression/issues/6
t.Parallel()

cases := map[string]struct {
contentType string
writeHeader bool
_range string
acceptEncoding string
body string

expectRange string
expectAcceptRanges string
expectContentEncoding string
}{
// if the response is compressed, we do not support accept-ranges/range
"supported-encoding range": {"text/plain", false, "100-110bytes", "gzip", testBody, "", "", "gzip"},
"supported-encoding range explicit-writeheader": {"text/plain", true, "100-110bytes", "gzip", testBody, "", "", "gzip"},
// if the client does not accept one of the enabled encodings, we support accept-ranges/range
"unsupported-encoding range": {"text/plain", false, "100-110bytes", "unknown", testBody, "100-110bytes", "bytes", ""},
"unsupported-encoding range explicit-writeheader": {"text/plain", true, "100-110bytes", "unknown", testBody, "100-110bytes", "bytes", ""},
// if the content-type is not allowed to be compressed, we still strip the accept-ranges/range headers
// because we can't know this until the handler starts writing the response. See also the comments in adapter.go.
"not-whitelisted-type range": {"unknown/type", false, "100-110bytes", "gzip", testBody, "", "", ""},
"not-whitelisted-type range explicit-writeheader": {"unknown/type", true, "100-110bytes", "gzip", testBody, "", "", ""},
}

for n, c := range cases {
c := c
t.Run(n, func(t *testing.T) {
t.Parallel()

handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, c.expectRange, r.Header.Get("Range"))
w.Header().Set(contentType, c.contentType)
w.Header().Set(acceptRanges, "bytes")
if c.writeHeader {
w.WriteHeader(http.StatusOK)
}
w.Write([]byte(c.body))
})

wrapper, err := DefaultAdapter(ContentTypes([]string{"text/plain"}, false))
assert.Nil(t, err, "DefaultAdapter returned error")

req, _ := http.NewRequest("GET", "/", nil)
req.Header.Set("Accept-Encoding", c.acceptEncoding)
req.Header.Set("Range", c._range)
resp := httptest.NewRecorder()
wrapper(handler).ServeHTTP(resp, req)
res := resp.Result()

assert.Equal(t, 200, res.StatusCode)
assert.Equal(t, c.expectAcceptRanges, res.Header.Get(acceptRanges))
assert.Equal(t, c.expectContentEncoding, res.Header.Get(contentEncoding))
})
}
}

// --------------------------------------------------------------------

const (
Expand Down
7 changes: 7 additions & 0 deletions response_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ func (w *compressWriter) startCompress(enc string) error {
// See: https://github.com/golang/go/issues/14975.
w.Header().Del(contentLength)

// See the comment about ranges in adapter.go
w.Header().Del(acceptRanges)

// Write the header to gzip response.
if w.code != 0 {
w.ResponseWriter.WriteHeader(w.code)
Expand Down Expand Up @@ -176,6 +179,10 @@ func (w *compressWriter) startCompress(enc string) error {

// startPlain writes to sent bytes and buffer the underlying ResponseWriter without gzip.
func (w *compressWriter) startPlain() error {
// See the comment about ranges in adapter.go; we need to do it even in this case
// because adapter will strip the range header anyway.
w.Header().Del(acceptRanges)

if w.code != 0 {
w.ResponseWriter.WriteHeader(w.code)
// Ensure that no other WriteHeader's happen
Expand Down

0 comments on commit 5e6055a

Please sign in to comment.