1
0
Fork 0

Add encodings option to the compression middleware

This commit is contained in:
Wolfgang Ellsässer 2024-08-07 16:20:04 +02:00 committed by GitHub
parent b611f967b7
commit 75881359ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 389 additions and 92 deletions

View file

@ -22,13 +22,18 @@ type Encoding struct {
Weight *float64
}
func getCompressionType(acceptEncoding []string, defaultType string) string {
if defaultType == "" {
// Keeps the pre-existing default inside Traefik.
defaultType = brotliName
func getCompressionEncoding(acceptEncoding []string, defaultEncoding string, supportedEncodings []string) string {
if defaultEncoding == "" {
if slices.Contains(supportedEncodings, brotliName) {
// Keeps the pre-existing default inside Traefik if brotli is a supported encoding.
defaultEncoding = brotliName
} else if len(supportedEncodings) > 0 {
// Otherwise use the first supported encoding.
defaultEncoding = supportedEncodings[0]
}
}
encodings, hasWeight := parseAcceptEncoding(acceptEncoding)
encodings, hasWeight := parseAcceptEncoding(acceptEncoding, supportedEncodings)
if hasWeight {
if len(encodings) == 0 {
@ -46,26 +51,26 @@ func getCompressionType(acceptEncoding []string, defaultType string) string {
}
if encoding.Type == wildcardName {
return defaultType
return defaultEncoding
}
return encoding.Type
}
for _, dt := range []string{zstdName, brotliName, gzipName} {
for _, dt := range supportedEncodings {
if slices.ContainsFunc(encodings, func(e Encoding) bool { return e.Type == dt }) {
return dt
}
}
if slices.ContainsFunc(encodings, func(e Encoding) bool { return e.Type == wildcardName }) {
return defaultType
return defaultEncoding
}
return identityName
}
func parseAcceptEncoding(acceptEncoding []string) ([]Encoding, bool) {
func parseAcceptEncoding(acceptEncoding, supportedEncodings []string) ([]Encoding, bool) {
var encodings []Encoding
var hasWeight bool
@ -76,10 +81,9 @@ func parseAcceptEncoding(acceptEncoding []string) ([]Encoding, bool) {
continue
}
switch parsed[0] {
case zstdName, brotliName, gzipName, identityName, wildcardName:
// supported encoding
default:
if !slices.Contains(supportedEncodings, parsed[0]) &&
parsed[0] != identityName &&
parsed[0] != wildcardName {
continue
}

View file

@ -6,73 +6,86 @@ import (
"github.com/stretchr/testify/assert"
)
func Test_getCompressionType(t *testing.T) {
func Test_getCompressionEncoding(t *testing.T) {
testCases := []struct {
desc string
values []string
defaultType string
expected string
desc string
acceptEncoding []string
defaultEncoding string
supportedEncodings []string
expected string
}{
{
desc: "br > gzip (no weight)",
values: []string{"gzip, br"},
expected: brotliName,
desc: "br > gzip (no weight)",
acceptEncoding: []string{"gzip, br"},
expected: brotliName,
},
{
desc: "zstd > br > gzip (no weight)",
values: []string{"zstd, gzip, br"},
expected: zstdName,
desc: "zstd > br > gzip (no weight)",
acceptEncoding: []string{"zstd, gzip, br"},
expected: zstdName,
},
{
desc: "known compression type (no weight)",
values: []string{"compress, gzip"},
expected: gzipName,
desc: "known compression encoding (no weight)",
acceptEncoding: []string{"compress, gzip"},
expected: gzipName,
},
{
desc: "unknown compression type (no weight), no encoding",
values: []string{"compress, rar"},
expected: identityName,
desc: "unknown compression encoding (no weight), no encoding",
acceptEncoding: []string{"compress, rar"},
expected: identityName,
},
{
desc: "wildcard return the default compression type",
values: []string{"*"},
expected: brotliName,
desc: "wildcard return the default compression encoding",
acceptEncoding: []string{"*"},
expected: brotliName,
},
{
desc: "wildcard return the custom default compression type",
values: []string{"*"},
defaultType: "foo",
expected: "foo",
desc: "wildcard return the custom default compression encoding",
acceptEncoding: []string{"*"},
defaultEncoding: "foo",
expected: "foo",
},
{
desc: "follows weight",
values: []string{"br;q=0.8, gzip;q=1.0, *;q=0.1"},
expected: gzipName,
desc: "follows weight",
acceptEncoding: []string{"br;q=0.8, gzip;q=1.0, *;q=0.1"},
expected: gzipName,
},
{
desc: "ignore unknown compression type",
values: []string{"compress;q=1.0, gzip;q=0.5"},
expected: gzipName,
desc: "ignore unknown compression encoding",
acceptEncoding: []string{"compress;q=1.0, gzip;q=0.5"},
expected: gzipName,
},
{
desc: "fallback on non-zero compression type",
values: []string{"compress;q=1.0, gzip, identity;q=0"},
expected: gzipName,
desc: "fallback on non-zero compression encoding",
acceptEncoding: []string{"compress;q=1.0, gzip, identity;q=0"},
expected: gzipName,
},
{
desc: "not acceptable (identity)",
values: []string{"compress;q=1.0, identity;q=0"},
expected: notAcceptable,
desc: "not acceptable (identity)",
acceptEncoding: []string{"compress;q=1.0, identity;q=0"},
expected: notAcceptable,
},
{
desc: "not acceptable (wildcard)",
values: []string{"compress;q=1.0, *;q=0"},
expected: notAcceptable,
desc: "not acceptable (wildcard)",
acceptEncoding: []string{"compress;q=1.0, *;q=0"},
expected: notAcceptable,
},
{
desc: "non-zero is higher than 0",
values: []string{"gzip, *;q=0"},
expected: gzipName,
desc: "non-zero is higher than 0",
acceptEncoding: []string{"gzip, *;q=0"},
expected: gzipName,
},
{
desc: "zstd forbidden, brotli first",
acceptEncoding: []string{"zstd, gzip, br"},
supportedEncodings: []string{brotliName, gzipName},
expected: brotliName,
},
{
desc: "follows weight, ignores forbidden encoding",
acceptEncoding: []string{"br;q=0.8, gzip;q=1.0, *;q=0.1"},
supportedEncodings: []string{zstdName, brotliName},
expected: brotliName,
},
}
@ -80,19 +93,24 @@ func Test_getCompressionType(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
encodingType := getCompressionType(test.values, test.defaultType)
if test.supportedEncodings == nil {
test.supportedEncodings = defaultSupportedEncodings
}
assert.Equal(t, test.expected, encodingType)
encoding := getCompressionEncoding(test.acceptEncoding, test.defaultEncoding, test.supportedEncodings)
assert.Equal(t, test.expected, encoding)
})
}
}
func Test_parseAcceptEncoding(t *testing.T) {
testCases := []struct {
desc string
values []string
expected []Encoding
assertWeight assert.BoolAssertionFunc
desc string
values []string
supportedEncodings []string
expected []Encoding
assertWeight assert.BoolAssertionFunc
}{
{
desc: "weight",
@ -105,6 +123,17 @@ func Test_parseAcceptEncoding(t *testing.T) {
},
assertWeight: assert.True,
},
{
desc: "weight with supported encodings",
values: []string{"br;q=1.0, zstd;q=0.9, gzip;q=0.8, *;q=0.1"},
supportedEncodings: []string{brotliName, gzipName},
expected: []Encoding{
{Type: brotliName, Weight: ptr[float64](1)},
{Type: gzipName, Weight: ptr(0.8)},
{Type: wildcardName, Weight: ptr(0.1)},
},
assertWeight: assert.True,
},
{
desc: "mixed",
values: []string{"zstd,gzip, br;q=1.0, *;q=0"},
@ -116,6 +145,16 @@ func Test_parseAcceptEncoding(t *testing.T) {
},
assertWeight: assert.True,
},
{
desc: "mixed with supported encodings",
values: []string{"zstd,gzip, br;q=1.0, *;q=0"},
supportedEncodings: []string{zstdName},
expected: []Encoding{
{Type: zstdName},
{Type: wildcardName, Weight: ptr[float64](0)},
},
assertWeight: assert.True,
},
{
desc: "no weight",
values: []string{"zstd, gzip, br, *"},
@ -127,6 +166,16 @@ func Test_parseAcceptEncoding(t *testing.T) {
},
assertWeight: assert.False,
},
{
desc: "no weight with supported encodings",
values: []string{"zstd, gzip, br, *"},
supportedEncodings: []string{"gzip"},
expected: []Encoding{
{Type: gzipName},
{Type: wildcardName},
},
assertWeight: assert.False,
},
{
desc: "weight and identity",
values: []string{"gzip;q=1.0, identity; q=0.5, *;q=0"},
@ -137,13 +186,27 @@ func Test_parseAcceptEncoding(t *testing.T) {
},
assertWeight: assert.True,
},
{
desc: "weight and identity",
values: []string{"gzip;q=1.0, identity; q=0.5, *;q=0"},
supportedEncodings: []string{"br"},
expected: []Encoding{
{Type: identityName, Weight: ptr(0.5)},
{Type: wildcardName, Weight: ptr[float64](0)},
},
assertWeight: assert.True,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
aes, hasWeight := parseAcceptEncoding(test.values)
if test.supportedEncodings == nil {
test.supportedEncodings = defaultSupportedEncodings
}
aes, hasWeight := parseAcceptEncoding(test.values, test.supportedEncodings)
assert.Equal(t, test.expected, aes)
test.assertWeight(t, hasWeight)

View file

@ -16,9 +16,11 @@ import (
const typeName = "Compress"
// DefaultMinSize is the default minimum size (in bytes) required to enable compression.
// defaultMinSize is the default minimum size (in bytes) required to enable compression.
// See https://github.com/klauspost/compress/blob/9559b037e79ad673c71f6ef7c732c00949014cd2/gzhttp/compress.go#L47.
const DefaultMinSize = 1024
const defaultMinSize = 1024
var defaultSupportedEncodings = []string{zstdName, brotliName, gzipName}
// Compress is a middleware that allows to compress the response.
type compress struct {
@ -27,6 +29,7 @@ type compress struct {
excludes []string
includes []string
minSize int
encodings []string
defaultEncoding string
brotliHandler http.Handler
@ -62,17 +65,30 @@ func New(ctx context.Context, next http.Handler, conf dynamic.Compress, name str
includes = append(includes, mediaType)
}
minSize := DefaultMinSize
minSize := defaultMinSize
if conf.MinResponseBodyBytes > 0 {
minSize = conf.MinResponseBodyBytes
}
if len(conf.Encodings) == 0 {
return nil, errors.New("at least one encoding must be specified")
}
for _, encoding := range conf.Encodings {
if !slices.Contains(defaultSupportedEncodings, encoding) {
return nil, fmt.Errorf("unsupported encoding: %s", encoding)
}
}
if conf.DefaultEncoding != "" && !slices.Contains(conf.Encodings, conf.DefaultEncoding) {
return nil, fmt.Errorf("unsupported default encoding: %s", conf.DefaultEncoding)
}
c := &compress{
next: next,
name: name,
excludes: excludes,
includes: includes,
minSize: minSize,
encodings: conf.Encodings,
defaultEncoding: conf.DefaultEncoding,
}
@ -131,7 +147,7 @@ func (c *compress) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
return
}
c.chooseHandler(getCompressionType(acceptEncoding, c.defaultEncoding), rw, req)
c.chooseHandler(getCompressionEncoding(acceptEncoding, c.defaultEncoding, c.encodings), rw, req)
}
func (c *compress) chooseHandler(typ string, rw http.ResponseWriter, req *http.Request) {

View file

@ -102,7 +102,11 @@ func TestNegotiation(t *testing.T) {
next := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
_, _ = rw.Write(generateBytes(10))
})
handler, err := New(context.Background(), next, dynamic.Compress{MinResponseBodyBytes: 1}, "testing")
cfg := dynamic.Compress{
MinResponseBodyBytes: 1,
Encodings: defaultSupportedEncodings,
}
handler, err := New(context.Background(), next, cfg, "testing")
require.NoError(t, err)
rw := httptest.NewRecorder()
@ -123,7 +127,7 @@ func TestShouldCompressWhenNoContentEncodingHeader(t *testing.T) {
_, err := rw.Write(baseBody)
assert.NoError(t, err)
})
handler, err := New(context.Background(), next, dynamic.Compress{}, "testing")
handler, err := New(context.Background(), next, dynamic.Compress{Encodings: defaultSupportedEncodings}, "testing")
require.NoError(t, err)
rw := httptest.NewRecorder()
@ -153,7 +157,7 @@ func TestShouldNotCompressWhenContentEncodingHeader(t *testing.T) {
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
})
handler, err := New(context.Background(), next, dynamic.Compress{}, "testing")
handler, err := New(context.Background(), next, dynamic.Compress{Encodings: defaultSupportedEncodings}, "testing")
require.NoError(t, err)
rw := httptest.NewRecorder()
@ -175,7 +179,7 @@ func TestShouldNotCompressWhenNoAcceptEncodingHeader(t *testing.T) {
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
})
handler, err := New(context.Background(), next, dynamic.Compress{}, "testing")
handler, err := New(context.Background(), next, dynamic.Compress{Encodings: defaultSupportedEncodings}, "testing")
require.NoError(t, err)
rw := httptest.NewRecorder()
@ -202,7 +206,7 @@ func TestShouldNotCompressWhenIdentityAcceptEncodingHeader(t *testing.T) {
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
})
handler, err := New(context.Background(), next, dynamic.Compress{}, "testing")
handler, err := New(context.Background(), next, dynamic.Compress{Encodings: defaultSupportedEncodings}, "testing")
require.NoError(t, err)
rw := httptest.NewRecorder()
@ -229,7 +233,7 @@ func TestShouldNotCompressWhenEmptyAcceptEncodingHeader(t *testing.T) {
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
})
handler, err := New(context.Background(), next, dynamic.Compress{}, "testing")
handler, err := New(context.Background(), next, dynamic.Compress{Encodings: defaultSupportedEncodings}, "testing")
require.NoError(t, err)
rw := httptest.NewRecorder()
@ -251,7 +255,7 @@ func TestShouldNotCompressHeadRequest(t *testing.T) {
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
})
handler, err := New(context.Background(), next, dynamic.Compress{}, "testing")
handler, err := New(context.Background(), next, dynamic.Compress{Encodings: defaultSupportedEncodings}, "testing")
require.NoError(t, err)
rw := httptest.NewRecorder()
@ -274,6 +278,7 @@ func TestShouldNotCompressWhenSpecificContentType(t *testing.T) {
{
desc: "Exclude Request Content-Type",
conf: dynamic.Compress{
Encodings: defaultSupportedEncodings,
ExcludedContentTypes: []string{"text/event-stream"},
},
reqContentType: "text/event-stream",
@ -281,6 +286,7 @@ func TestShouldNotCompressWhenSpecificContentType(t *testing.T) {
{
desc: "Exclude Response Content-Type",
conf: dynamic.Compress{
Encodings: defaultSupportedEncodings,
ExcludedContentTypes: []string{"text/event-stream"},
},
respContentType: "text/event-stream",
@ -288,6 +294,7 @@ func TestShouldNotCompressWhenSpecificContentType(t *testing.T) {
{
desc: "Include Response Content-Type",
conf: dynamic.Compress{
Encodings: defaultSupportedEncodings,
IncludedContentTypes: []string{"text/plain"},
},
respContentType: "text/html",
@ -295,6 +302,7 @@ func TestShouldNotCompressWhenSpecificContentType(t *testing.T) {
{
desc: "Ignoring application/grpc with exclude option",
conf: dynamic.Compress{
Encodings: defaultSupportedEncodings,
ExcludedContentTypes: []string{"application/json"},
},
reqContentType: "application/grpc",
@ -302,13 +310,16 @@ func TestShouldNotCompressWhenSpecificContentType(t *testing.T) {
{
desc: "Ignoring application/grpc with include option",
conf: dynamic.Compress{
Encodings: defaultSupportedEncodings,
IncludedContentTypes: []string{"application/json"},
},
reqContentType: "application/grpc",
},
{
desc: "Ignoring application/grpc with no option",
conf: dynamic.Compress{},
desc: "Ignoring application/grpc with no option",
conf: dynamic.Compress{
Encodings: defaultSupportedEncodings,
},
reqContentType: "application/grpc",
},
}
@ -358,6 +369,7 @@ func TestShouldCompressWhenSpecificContentType(t *testing.T) {
{
desc: "Include Response Content-Type",
conf: dynamic.Compress{
Encodings: defaultSupportedEncodings,
IncludedContentTypes: []string{"text/html"},
},
respContentType: "text/html",
@ -429,7 +441,7 @@ func TestIntegrationShouldNotCompress(t *testing.T) {
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
compress, err := New(context.Background(), test.handler, dynamic.Compress{}, "testing")
compress, err := New(context.Background(), test.handler, dynamic.Compress{Encodings: defaultSupportedEncodings}, "testing")
require.NoError(t, err)
ts := httptest.NewServer(compress)
@ -464,7 +476,7 @@ func TestShouldWriteHeaderWhenFlush(t *testing.T) {
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
})
handler, err := New(context.Background(), next, dynamic.Compress{}, "testing")
handler, err := New(context.Background(), next, dynamic.Compress{Encodings: defaultSupportedEncodings}, "testing")
require.NoError(t, err)
ts := httptest.NewServer(handler)
@ -515,7 +527,7 @@ func TestIntegrationShouldCompress(t *testing.T) {
for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
compress, err := New(context.Background(), test.handler, dynamic.Compress{}, "testing")
compress, err := New(context.Background(), test.handler, dynamic.Compress{Encodings: defaultSupportedEncodings}, "testing")
require.NoError(t, err)
ts := httptest.NewServer(compress)
@ -571,8 +583,11 @@ func TestMinResponseBodyBytes(t *testing.T) {
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
})
handler, err := New(context.Background(), next, dynamic.Compress{MinResponseBodyBytes: test.minResponseBodyBytes}, "testing")
cfg := dynamic.Compress{
MinResponseBodyBytes: test.minResponseBodyBytes,
Encodings: defaultSupportedEncodings,
}
handler, err := New(context.Background(), next, cfg, "testing")
require.NoError(t, err)
rw := httptest.NewRecorder()
@ -607,8 +622,11 @@ func Test1xxResponses(t *testing.T) {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
compress, err := New(context.Background(), next, dynamic.Compress{MinResponseBodyBytes: 1024}, "testing")
cfg := dynamic.Compress{
MinResponseBodyBytes: 1024,
Encodings: defaultSupportedEncodings,
}
compress, err := New(context.Background(), next, cfg, "testing")
require.NoError(t, err)
server := httptest.NewServer(compress)