Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion middleware/compress.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,54 @@ const (
gzipScheme = "gzip"
)

// acceptsGzip reports whether the given Accept-Encoding header value indicates
// that the client accepts the gzip coding.
//
// It parses the header according to RFC 9110 §12.5.3 instead of doing a naive
// substring match. This avoids two classes of bugs:
// - "gzip;q=0" explicitly means the client does NOT want gzip, yet a substring
// match would still enable compression.
// - tokens such as "x-gzip" or "supergzip" would substring-match "gzip" even
// though they are distinct codings.
//
// A "*" coding with a non-zero q-value is also treated as accepting gzip.
func acceptsGzip(acceptEncoding string) bool {
if acceptEncoding == "" {
return false
}
for _, part := range strings.Split(acceptEncoding, ",") {
coding, qvalue := parseCoding(part)
if coding != gzipScheme && coding != "*" {
continue
}
if qvalue == "0" || qvalue == "0.0" || qvalue == "0.00" || qvalue == "0.000" {
// q=0 means "not acceptable"; an explicit gzip rejection wins.
if coding == gzipScheme {
return false
}
continue
}
return true
}
return false
}
Comment on lines +35 to +54

// parseCoding splits a single Accept-Encoding element into its coding name
// (lower-cased) and its q-value parameter (empty string when absent).
func parseCoding(part string) (coding, qvalue string) {
coding = part
if idx := strings.IndexByte(part, ';'); idx >= 0 {
coding = part[:idx]
for _, param := range strings.Split(part[idx+1:], ";") {
param = strings.TrimSpace(param)
if k, v, ok := strings.Cut(param, "="); ok && strings.EqualFold(strings.TrimSpace(k), "q") {
qvalue = strings.TrimSpace(v)
}
}
}
return strings.ToLower(strings.TrimSpace(coding)), qvalue
}

// GzipConfig defines the config for Gzip middleware.
type GzipConfig struct {
// Skipper defines a function to skip middleware.
Expand Down Expand Up @@ -91,7 +139,7 @@ func (config GzipConfig) ToMiddleware() (echo.MiddlewareFunc, error) {

res := c.Response()
res.Header().Add(echo.HeaderVary, echo.HeaderAcceptEncoding)
if strings.Contains(c.Request().Header.Get(echo.HeaderAcceptEncoding), gzipScheme) {
if acceptsGzip(c.Request().Header.Get(echo.HeaderAcceptEncoding)) {
i := pool.Get()
w, ok := i.(*gzip.Writer)
if !ok {
Expand Down
59 changes: 59 additions & 0 deletions middleware/compress_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -396,3 +396,62 @@ func BenchmarkGzip(b *testing.B) {
h(c)
}
}

func TestGzip_AcceptEncodingQValue(t *testing.T) {
// RFC 9110 §12.5.3: "gzip;q=0" means the client does not accept gzip, and a
// token like "supergzip"/"x-gzip" must not be mistaken for "gzip".
isGzipped := func(acceptEncoding string) bool {
h := Gzip()(func(c *echo.Context) error {
c.Response().Write([]byte("test")) // For Content-Type sniffing
return nil
})
e := echo.New()
req := httptest.NewRequest(http.MethodGet, "/", nil)
if acceptEncoding != "" {
req.Header.Set(echo.HeaderAcceptEncoding, acceptEncoding)
}
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
assert.NoError(t, h(c))
return rec.Header().Get(echo.HeaderContentEncoding) == gzipScheme
}

tests := []struct {
acceptEncoding string
wantGzip bool
}{
{"gzip", true},
{"gzip;q=1.0", true},
{"gzip;q=0.5", true},
{"deflate, gzip", true},
{"deflate, gzip;q=0.8", true},
{"*", true},
{"gzip;q=0", false}, // explicit rejection
{"gzip;q=0.0", false}, // explicit rejection
{"deflate, gzip;q=0", false},
{"identity", false},
{"supergzip", false}, // must not substring-match
{"x-gzip", false}, // distinct coding, not "gzip"
{"deflate", false},
{"", false},
}
for _, tt := range tests {
assert.Equalf(t, tt.wantGzip, isGzipped(tt.acceptEncoding),
"Accept-Encoding %q: expected gzip=%v", tt.acceptEncoding, tt.wantGzip)
}
}

func TestAcceptsGzip(t *testing.T) {
assert.True(t, acceptsGzip("gzip"))
assert.True(t, acceptsGzip(" GZIP "))
assert.True(t, acceptsGzip("br, gzip;q=0.9"))
assert.True(t, acceptsGzip("*"))
assert.True(t, acceptsGzip("*;q=0.1"))
assert.False(t, acceptsGzip(""))
assert.False(t, acceptsGzip("gzip;q=0"))
assert.False(t, acceptsGzip("gzip; q=0.000"))
assert.False(t, acceptsGzip("identity"))
assert.False(t, acceptsGzip("supergzip"))
assert.False(t, acceptsGzip("x-gzip"))
assert.False(t, acceptsGzip("*;q=0"))
}
Comment on lines +444 to +457