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

@ -255,3 +255,48 @@ http:
[http.middlewares.test-compress.compress] [http.middlewares.test-compress.compress]
defaultEncoding = "gzip" defaultEncoding = "gzip"
``` ```
### `encodings`
_Optional, Default="zstd, br, gzip"_
`encodings` specifies the list of supported compression encodings.
At least one encoding value must be specified, and valid entries are `zstd` (Zstandard), `br` (Brotli), and `gzip` (Gzip).
The order of the list also sets the priority, the top entry has the highest priority.
```yaml tab="Docker & Swarm"
labels:
- "traefik.http.middlewares.test-compress.compress.encodings=zstd,br"
```
```yaml tab="Kubernetes"
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: test-compress
spec:
compress:
encodings:
- zstd
- br
```
```yaml tab="Consul Catalog"
- "traefik.http.middlewares.test-compress.compress.encodings=zstd,br"
```
```yaml tab="File (YAML)"
http:
middlewares:
test-compress:
compress:
encodings:
- zstd
- br
```
```toml tab="File (TOML)"
[http.middlewares]
[http.middlewares.test-compress.compress]
encodings = ["zstd","br"]
```

View file

@ -19,6 +19,7 @@
- "traefik.http.middlewares.middleware05.circuitbreaker.responsecode=42" - "traefik.http.middlewares.middleware05.circuitbreaker.responsecode=42"
- "traefik.http.middlewares.middleware06.compress=true" - "traefik.http.middlewares.middleware06.compress=true"
- "traefik.http.middlewares.middleware06.compress.defaultencoding=foobar" - "traefik.http.middlewares.middleware06.compress.defaultencoding=foobar"
- "traefik.http.middlewares.middleware06.compress.encodings=foobar, foobar"
- "traefik.http.middlewares.middleware06.compress.excludedcontenttypes=foobar, foobar" - "traefik.http.middlewares.middleware06.compress.excludedcontenttypes=foobar, foobar"
- "traefik.http.middlewares.middleware06.compress.includedcontenttypes=foobar, foobar" - "traefik.http.middlewares.middleware06.compress.includedcontenttypes=foobar, foobar"
- "traefik.http.middlewares.middleware06.compress.minresponsebodybytes=42" - "traefik.http.middlewares.middleware06.compress.minresponsebodybytes=42"

View file

@ -143,6 +143,7 @@
excludedContentTypes = ["foobar", "foobar"] excludedContentTypes = ["foobar", "foobar"]
includedContentTypes = ["foobar", "foobar"] includedContentTypes = ["foobar", "foobar"]
minResponseBodyBytes = 42 minResponseBodyBytes = 42
encodings = ["foobar", "foobar"]
defaultEncoding = "foobar" defaultEncoding = "foobar"
[http.middlewares.Middleware07] [http.middlewares.Middleware07]
[http.middlewares.Middleware07.contentType] [http.middlewares.Middleware07.contentType]

View file

@ -152,6 +152,9 @@ http:
- foobar - foobar
- foobar - foobar
minResponseBodyBytes: 42 minResponseBodyBytes: 42
encodings:
- foobar
- foobar
defaultEncoding: foobar defaultEncoding: foobar
Middleware07: Middleware07:
contentType: contentType:

View file

@ -904,7 +904,7 @@ spec:
compress: compress:
description: |- description: |-
Compress holds the compress middleware configuration. Compress holds the compress middleware configuration.
This middleware compresses responses before sending them to the client, using gzip compression. This middleware compresses responses before sending them to the client, using gzip, brotli, or zstd compression.
More info: https://doc.traefik.io/traefik/v3.1/middlewares/http/compress/ More info: https://doc.traefik.io/traefik/v3.1/middlewares/http/compress/
properties: properties:
defaultEncoding: defaultEncoding:
@ -912,6 +912,12 @@ spec:
the `Accept-Encoding` header is not in the request or contains the `Accept-Encoding` header is not in the request or contains
a wildcard (`*`). a wildcard (`*`).
type: string type: string
encodings:
description: Encodings defines the list of supported compression
algorithms.
items:
type: string
type: array
excludedContentTypes: excludedContentTypes:
description: |- description: |-
ExcludedContentTypes defines the list of content types to compare the Content-Type header of the incoming requests and responses before compressing. ExcludedContentTypes defines the list of content types to compare the Content-Type header of the incoming requests and responses before compressing.

View file

@ -22,6 +22,8 @@ THIS FILE MUST NOT BE EDITED BY HAND
| `traefik/http/middlewares/Middleware05/circuitBreaker/recoveryDuration` | `42s` | | `traefik/http/middlewares/Middleware05/circuitBreaker/recoveryDuration` | `42s` |
| `traefik/http/middlewares/Middleware05/circuitBreaker/responseCode` | `42` | | `traefik/http/middlewares/Middleware05/circuitBreaker/responseCode` | `42` |
| `traefik/http/middlewares/Middleware06/compress/defaultEncoding` | `foobar` | | `traefik/http/middlewares/Middleware06/compress/defaultEncoding` | `foobar` |
| `traefik/http/middlewares/Middleware06/compress/encodings/0` | `foobar` |
| `traefik/http/middlewares/Middleware06/compress/encodings/1` | `foobar` |
| `traefik/http/middlewares/Middleware06/compress/excludedContentTypes/0` | `foobar` | | `traefik/http/middlewares/Middleware06/compress/excludedContentTypes/0` | `foobar` |
| `traefik/http/middlewares/Middleware06/compress/excludedContentTypes/1` | `foobar` | | `traefik/http/middlewares/Middleware06/compress/excludedContentTypes/1` | `foobar` |
| `traefik/http/middlewares/Middleware06/compress/includedContentTypes/0` | `foobar` | | `traefik/http/middlewares/Middleware06/compress/includedContentTypes/0` | `foobar` |

View file

@ -180,7 +180,7 @@ spec:
compress: compress:
description: |- description: |-
Compress holds the compress middleware configuration. Compress holds the compress middleware configuration.
This middleware compresses responses before sending them to the client, using gzip compression. This middleware compresses responses before sending them to the client, using gzip, brotli, or zstd compression.
More info: https://doc.traefik.io/traefik/v3.1/middlewares/http/compress/ More info: https://doc.traefik.io/traefik/v3.1/middlewares/http/compress/
properties: properties:
defaultEncoding: defaultEncoding:
@ -188,6 +188,12 @@ spec:
the `Accept-Encoding` header is not in the request or contains the `Accept-Encoding` header is not in the request or contains
a wildcard (`*`). a wildcard (`*`).
type: string type: string
encodings:
description: Encodings defines the list of supported compression
algorithms.
items:
type: string
type: array
excludedContentTypes: excludedContentTypes:
description: |- description: |-
ExcludedContentTypes defines the list of content types to compare the Content-Type header of the incoming requests and responses before compressing. ExcludedContentTypes defines the list of content types to compare the Content-Type header of the incoming requests and responses before compressing.

View file

@ -904,7 +904,7 @@ spec:
compress: compress:
description: |- description: |-
Compress holds the compress middleware configuration. Compress holds the compress middleware configuration.
This middleware compresses responses before sending them to the client, using gzip compression. This middleware compresses responses before sending them to the client, using gzip, brotli, or zstd compression.
More info: https://doc.traefik.io/traefik/v3.1/middlewares/http/compress/ More info: https://doc.traefik.io/traefik/v3.1/middlewares/http/compress/
properties: properties:
defaultEncoding: defaultEncoding:
@ -912,6 +912,12 @@ spec:
the `Accept-Encoding` header is not in the request or contains the `Accept-Encoding` header is not in the request or contains
a wildcard (`*`). a wildcard (`*`).
type: string type: string
encodings:
description: Encodings defines the list of supported compression
algorithms.
items:
type: string
type: array
excludedContentTypes: excludedContentTypes:
description: |- description: |-
ExcludedContentTypes defines the list of content types to compare the Content-Type header of the incoming requests and responses before compressing. ExcludedContentTypes defines the list of content types to compare the Content-Type header of the incoming requests and responses before compressing.

View file

@ -165,8 +165,7 @@ func (c *CircuitBreaker) SetDefaults() {
// +k8s:deepcopy-gen=true // +k8s:deepcopy-gen=true
// Compress holds the compress middleware configuration. // Compress holds the compress middleware configuration.
// This middleware compresses responses before sending them to the client, using gzip compression. // This middleware compresses responses before sending them to the client, using gzip, brotli, or zstd compression.
// More info: https://doc.traefik.io/traefik/v3.1/middlewares/http/compress/
type Compress struct { type Compress struct {
// ExcludedContentTypes defines the list of content types to compare the Content-Type header of the incoming requests and responses before compressing. // ExcludedContentTypes defines the list of content types to compare the Content-Type header of the incoming requests and responses before compressing.
// `application/grpc` is always excluded. // `application/grpc` is always excluded.
@ -176,10 +175,16 @@ type Compress struct {
// MinResponseBodyBytes defines the minimum amount of bytes a response body must have to be compressed. // MinResponseBodyBytes defines the minimum amount of bytes a response body must have to be compressed.
// Default: 1024. // Default: 1024.
MinResponseBodyBytes int `json:"minResponseBodyBytes,omitempty" toml:"minResponseBodyBytes,omitempty" yaml:"minResponseBodyBytes,omitempty" export:"true"` MinResponseBodyBytes int `json:"minResponseBodyBytes,omitempty" toml:"minResponseBodyBytes,omitempty" yaml:"minResponseBodyBytes,omitempty" export:"true"`
// Encodings defines the list of supported compression algorithms.
Encodings []string `json:"encodings,omitempty" toml:"encodings,omitempty" yaml:"encodings,omitempty" export:"true"`
// DefaultEncoding specifies the default encoding if the `Accept-Encoding` header is not in the request or contains a wildcard (`*`). // DefaultEncoding specifies the default encoding if the `Accept-Encoding` header is not in the request or contains a wildcard (`*`).
DefaultEncoding string `json:"defaultEncoding,omitempty" toml:"defaultEncoding,omitempty" yaml:"defaultEncoding,omitempty" export:"true"` DefaultEncoding string `json:"defaultEncoding,omitempty" toml:"defaultEncoding,omitempty" yaml:"defaultEncoding,omitempty" export:"true"`
} }
func (c *Compress) SetDefaults() {
c.Encodings = []string{"zstd", "br", "gzip"}
}
// +k8s:deepcopy-gen=true // +k8s:deepcopy-gen=true
// DigestAuth holds the digest auth middleware configuration. // DigestAuth holds the digest auth middleware configuration.

View file

@ -158,6 +158,11 @@ func (in *Compress) DeepCopyInto(out *Compress) {
*out = make([]string, len(*in)) *out = make([]string, len(*in))
copy(*out, *in) copy(*out, *in)
} }
if in.Encodings != nil {
in, out := &in.Encodings, &out.Encodings
*out = make([]string, len(*in))
copy(*out, *in)
}
return return
} }

View file

@ -137,6 +137,7 @@ func TestDecodeConfiguration(t *testing.T) {
"traefik.http.middlewares.Middleware17.stripprefix.prefixes": "foobar, fiibar", "traefik.http.middlewares.Middleware17.stripprefix.prefixes": "foobar, fiibar",
"traefik.http.middlewares.Middleware17.stripprefix.forceslash": "true", "traefik.http.middlewares.Middleware17.stripprefix.forceslash": "true",
"traefik.http.middlewares.Middleware18.stripprefixregex.regex": "foobar, fiibar", "traefik.http.middlewares.Middleware18.stripprefixregex.regex": "foobar, fiibar",
"traefik.http.middlewares.Middleware19.compress.encodings": "foobar, fiibar",
"traefik.http.middlewares.Middleware19.compress.minresponsebodybytes": "42", "traefik.http.middlewares.Middleware19.compress.minresponsebodybytes": "42",
"traefik.http.middlewares.Middleware20.plugin.tomato.aaa": "foo1", "traefik.http.middlewares.Middleware20.plugin.tomato.aaa": "foo1",
"traefik.http.middlewares.Middleware20.plugin.tomato.bbb": "foo2", "traefik.http.middlewares.Middleware20.plugin.tomato.bbb": "foo2",
@ -493,6 +494,10 @@ func TestDecodeConfiguration(t *testing.T) {
"Middleware19": { "Middleware19": {
Compress: &dynamic.Compress{ Compress: &dynamic.Compress{
MinResponseBodyBytes: 42, MinResponseBodyBytes: 42,
Encodings: []string{
"foobar",
"fiibar",
},
}, },
}, },
"Middleware2": { "Middleware2": {
@ -1009,6 +1014,10 @@ func TestEncodeConfiguration(t *testing.T) {
"Middleware19": { "Middleware19": {
Compress: &dynamic.Compress{ Compress: &dynamic.Compress{
MinResponseBodyBytes: 42, MinResponseBodyBytes: 42,
Encodings: []string{
"foobar",
"fiibar",
},
}, },
}, },
"Middleware2": { "Middleware2": {
@ -1377,6 +1386,7 @@ func TestEncodeConfiguration(t *testing.T) {
"traefik.HTTP.Middlewares.Middleware17.StripPrefix.Prefixes": "foobar, fiibar", "traefik.HTTP.Middlewares.Middleware17.StripPrefix.Prefixes": "foobar, fiibar",
"traefik.HTTP.Middlewares.Middleware17.StripPrefix.ForceSlash": "true", "traefik.HTTP.Middlewares.Middleware17.StripPrefix.ForceSlash": "true",
"traefik.HTTP.Middlewares.Middleware18.StripPrefixRegex.Regex": "foobar, fiibar", "traefik.HTTP.Middlewares.Middleware18.StripPrefixRegex.Regex": "foobar, fiibar",
"traefik.HTTP.Middlewares.Middleware19.Compress.Encodings": "foobar, fiibar",
"traefik.HTTP.Middlewares.Middleware19.Compress.MinResponseBodyBytes": "42", "traefik.HTTP.Middlewares.Middleware19.Compress.MinResponseBodyBytes": "42",
"traefik.HTTP.Middlewares.Middleware20.Plugin.tomato.aaa": "foo1", "traefik.HTTP.Middlewares.Middleware20.Plugin.tomato.aaa": "foo1",
"traefik.HTTP.Middlewares.Middleware20.Plugin.tomato.bbb": "foo2", "traefik.HTTP.Middlewares.Middleware20.Plugin.tomato.bbb": "foo2",

View file

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

View file

@ -6,83 +6,100 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func Test_getCompressionType(t *testing.T) { func Test_getCompressionEncoding(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
values []string acceptEncoding []string
defaultType string defaultEncoding string
supportedEncodings []string
expected string expected string
}{ }{
{ {
desc: "br > gzip (no weight)", desc: "br > gzip (no weight)",
values: []string{"gzip, br"}, acceptEncoding: []string{"gzip, br"},
expected: brotliName, expected: brotliName,
}, },
{ {
desc: "zstd > br > gzip (no weight)", desc: "zstd > br > gzip (no weight)",
values: []string{"zstd, gzip, br"}, acceptEncoding: []string{"zstd, gzip, br"},
expected: zstdName, expected: zstdName,
}, },
{ {
desc: "known compression type (no weight)", desc: "known compression encoding (no weight)",
values: []string{"compress, gzip"}, acceptEncoding: []string{"compress, gzip"},
expected: gzipName, expected: gzipName,
}, },
{ {
desc: "unknown compression type (no weight), no encoding", desc: "unknown compression encoding (no weight), no encoding",
values: []string{"compress, rar"}, acceptEncoding: []string{"compress, rar"},
expected: identityName, expected: identityName,
}, },
{ {
desc: "wildcard return the default compression type", desc: "wildcard return the default compression encoding",
values: []string{"*"}, acceptEncoding: []string{"*"},
expected: brotliName, expected: brotliName,
}, },
{ {
desc: "wildcard return the custom default compression type", desc: "wildcard return the custom default compression encoding",
values: []string{"*"}, acceptEncoding: []string{"*"},
defaultType: "foo", defaultEncoding: "foo",
expected: "foo", expected: "foo",
}, },
{ {
desc: "follows weight", desc: "follows weight",
values: []string{"br;q=0.8, gzip;q=1.0, *;q=0.1"}, acceptEncoding: []string{"br;q=0.8, gzip;q=1.0, *;q=0.1"},
expected: gzipName, expected: gzipName,
}, },
{ {
desc: "ignore unknown compression type", desc: "ignore unknown compression encoding",
values: []string{"compress;q=1.0, gzip;q=0.5"}, acceptEncoding: []string{"compress;q=1.0, gzip;q=0.5"},
expected: gzipName, expected: gzipName,
}, },
{ {
desc: "fallback on non-zero compression type", desc: "fallback on non-zero compression encoding",
values: []string{"compress;q=1.0, gzip, identity;q=0"}, acceptEncoding: []string{"compress;q=1.0, gzip, identity;q=0"},
expected: gzipName, expected: gzipName,
}, },
{ {
desc: "not acceptable (identity)", desc: "not acceptable (identity)",
values: []string{"compress;q=1.0, identity;q=0"}, acceptEncoding: []string{"compress;q=1.0, identity;q=0"},
expected: notAcceptable, expected: notAcceptable,
}, },
{ {
desc: "not acceptable (wildcard)", desc: "not acceptable (wildcard)",
values: []string{"compress;q=1.0, *;q=0"}, acceptEncoding: []string{"compress;q=1.0, *;q=0"},
expected: notAcceptable, expected: notAcceptable,
}, },
{ {
desc: "non-zero is higher than 0", desc: "non-zero is higher than 0",
values: []string{"gzip, *;q=0"}, acceptEncoding: []string{"gzip, *;q=0"},
expected: gzipName, 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,
},
} }
for _, test := range testCases { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() 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)
}) })
} }
} }
@ -91,6 +108,7 @@ func Test_parseAcceptEncoding(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string
values []string values []string
supportedEncodings []string
expected []Encoding expected []Encoding
assertWeight assert.BoolAssertionFunc assertWeight assert.BoolAssertionFunc
}{ }{
@ -105,6 +123,17 @@ func Test_parseAcceptEncoding(t *testing.T) {
}, },
assertWeight: assert.True, 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", desc: "mixed",
values: []string{"zstd,gzip, br;q=1.0, *;q=0"}, values: []string{"zstd,gzip, br;q=1.0, *;q=0"},
@ -116,6 +145,16 @@ func Test_parseAcceptEncoding(t *testing.T) {
}, },
assertWeight: assert.True, 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", desc: "no weight",
values: []string{"zstd, gzip, br, *"}, values: []string{"zstd, gzip, br, *"},
@ -127,6 +166,16 @@ func Test_parseAcceptEncoding(t *testing.T) {
}, },
assertWeight: assert.False, 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", desc: "weight and identity",
values: []string{"gzip;q=1.0, identity; q=0.5, *;q=0"}, 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, 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 { for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() 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) assert.Equal(t, test.expected, aes)
test.assertWeight(t, hasWeight) test.assertWeight(t, hasWeight)

View file

@ -16,9 +16,11 @@ import (
const typeName = "Compress" 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. // 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. // Compress is a middleware that allows to compress the response.
type compress struct { type compress struct {
@ -27,6 +29,7 @@ type compress struct {
excludes []string excludes []string
includes []string includes []string
minSize int minSize int
encodings []string
defaultEncoding string defaultEncoding string
brotliHandler http.Handler brotliHandler http.Handler
@ -62,17 +65,30 @@ func New(ctx context.Context, next http.Handler, conf dynamic.Compress, name str
includes = append(includes, mediaType) includes = append(includes, mediaType)
} }
minSize := DefaultMinSize minSize := defaultMinSize
if conf.MinResponseBodyBytes > 0 { if conf.MinResponseBodyBytes > 0 {
minSize = conf.MinResponseBodyBytes 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{ c := &compress{
next: next, next: next,
name: name, name: name,
excludes: excludes, excludes: excludes,
includes: includes, includes: includes,
minSize: minSize, minSize: minSize,
encodings: conf.Encodings,
defaultEncoding: conf.DefaultEncoding, defaultEncoding: conf.DefaultEncoding,
} }
@ -131,7 +147,7 @@ func (c *compress) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
return 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) { 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) { next := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
_, _ = rw.Write(generateBytes(10)) _, _ = 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) require.NoError(t, err)
rw := httptest.NewRecorder() rw := httptest.NewRecorder()
@ -123,7 +127,7 @@ func TestShouldCompressWhenNoContentEncodingHeader(t *testing.T) {
_, err := rw.Write(baseBody) _, err := rw.Write(baseBody)
assert.NoError(t, err) 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) require.NoError(t, err)
rw := httptest.NewRecorder() rw := httptest.NewRecorder()
@ -153,7 +157,7 @@ func TestShouldNotCompressWhenContentEncodingHeader(t *testing.T) {
http.Error(rw, err.Error(), http.StatusInternalServerError) 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) require.NoError(t, err)
rw := httptest.NewRecorder() rw := httptest.NewRecorder()
@ -175,7 +179,7 @@ func TestShouldNotCompressWhenNoAcceptEncodingHeader(t *testing.T) {
http.Error(rw, err.Error(), http.StatusInternalServerError) 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) require.NoError(t, err)
rw := httptest.NewRecorder() rw := httptest.NewRecorder()
@ -202,7 +206,7 @@ func TestShouldNotCompressWhenIdentityAcceptEncodingHeader(t *testing.T) {
http.Error(rw, err.Error(), http.StatusInternalServerError) 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) require.NoError(t, err)
rw := httptest.NewRecorder() rw := httptest.NewRecorder()
@ -229,7 +233,7 @@ func TestShouldNotCompressWhenEmptyAcceptEncodingHeader(t *testing.T) {
http.Error(rw, err.Error(), http.StatusInternalServerError) 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) require.NoError(t, err)
rw := httptest.NewRecorder() rw := httptest.NewRecorder()
@ -251,7 +255,7 @@ func TestShouldNotCompressHeadRequest(t *testing.T) {
http.Error(rw, err.Error(), http.StatusInternalServerError) 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) require.NoError(t, err)
rw := httptest.NewRecorder() rw := httptest.NewRecorder()
@ -274,6 +278,7 @@ func TestShouldNotCompressWhenSpecificContentType(t *testing.T) {
{ {
desc: "Exclude Request Content-Type", desc: "Exclude Request Content-Type",
conf: dynamic.Compress{ conf: dynamic.Compress{
Encodings: defaultSupportedEncodings,
ExcludedContentTypes: []string{"text/event-stream"}, ExcludedContentTypes: []string{"text/event-stream"},
}, },
reqContentType: "text/event-stream", reqContentType: "text/event-stream",
@ -281,6 +286,7 @@ func TestShouldNotCompressWhenSpecificContentType(t *testing.T) {
{ {
desc: "Exclude Response Content-Type", desc: "Exclude Response Content-Type",
conf: dynamic.Compress{ conf: dynamic.Compress{
Encodings: defaultSupportedEncodings,
ExcludedContentTypes: []string{"text/event-stream"}, ExcludedContentTypes: []string{"text/event-stream"},
}, },
respContentType: "text/event-stream", respContentType: "text/event-stream",
@ -288,6 +294,7 @@ func TestShouldNotCompressWhenSpecificContentType(t *testing.T) {
{ {
desc: "Include Response Content-Type", desc: "Include Response Content-Type",
conf: dynamic.Compress{ conf: dynamic.Compress{
Encodings: defaultSupportedEncodings,
IncludedContentTypes: []string{"text/plain"}, IncludedContentTypes: []string{"text/plain"},
}, },
respContentType: "text/html", respContentType: "text/html",
@ -295,6 +302,7 @@ func TestShouldNotCompressWhenSpecificContentType(t *testing.T) {
{ {
desc: "Ignoring application/grpc with exclude option", desc: "Ignoring application/grpc with exclude option",
conf: dynamic.Compress{ conf: dynamic.Compress{
Encodings: defaultSupportedEncodings,
ExcludedContentTypes: []string{"application/json"}, ExcludedContentTypes: []string{"application/json"},
}, },
reqContentType: "application/grpc", reqContentType: "application/grpc",
@ -302,13 +310,16 @@ func TestShouldNotCompressWhenSpecificContentType(t *testing.T) {
{ {
desc: "Ignoring application/grpc with include option", desc: "Ignoring application/grpc with include option",
conf: dynamic.Compress{ conf: dynamic.Compress{
Encodings: defaultSupportedEncodings,
IncludedContentTypes: []string{"application/json"}, IncludedContentTypes: []string{"application/json"},
}, },
reqContentType: "application/grpc", reqContentType: "application/grpc",
}, },
{ {
desc: "Ignoring application/grpc with no option", desc: "Ignoring application/grpc with no option",
conf: dynamic.Compress{}, conf: dynamic.Compress{
Encodings: defaultSupportedEncodings,
},
reqContentType: "application/grpc", reqContentType: "application/grpc",
}, },
} }
@ -358,6 +369,7 @@ func TestShouldCompressWhenSpecificContentType(t *testing.T) {
{ {
desc: "Include Response Content-Type", desc: "Include Response Content-Type",
conf: dynamic.Compress{ conf: dynamic.Compress{
Encodings: defaultSupportedEncodings,
IncludedContentTypes: []string{"text/html"}, IncludedContentTypes: []string{"text/html"},
}, },
respContentType: "text/html", respContentType: "text/html",
@ -429,7 +441,7 @@ func TestIntegrationShouldNotCompress(t *testing.T) {
for _, test := range testCases { for _, test := range testCases {
t.Run(test.name, func(t *testing.T) { 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) require.NoError(t, err)
ts := httptest.NewServer(compress) ts := httptest.NewServer(compress)
@ -464,7 +476,7 @@ func TestShouldWriteHeaderWhenFlush(t *testing.T) {
http.Error(rw, err.Error(), http.StatusInternalServerError) 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) require.NoError(t, err)
ts := httptest.NewServer(handler) ts := httptest.NewServer(handler)
@ -515,7 +527,7 @@ func TestIntegrationShouldCompress(t *testing.T) {
for _, test := range testCases { for _, test := range testCases {
t.Run(test.name, func(t *testing.T) { 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) require.NoError(t, err)
ts := httptest.NewServer(compress) ts := httptest.NewServer(compress)
@ -571,8 +583,11 @@ func TestMinResponseBodyBytes(t *testing.T) {
http.Error(rw, err.Error(), http.StatusInternalServerError) http.Error(rw, err.Error(), http.StatusInternalServerError)
} }
}) })
cfg := dynamic.Compress{
handler, err := New(context.Background(), next, dynamic.Compress{MinResponseBodyBytes: test.minResponseBodyBytes}, "testing") MinResponseBodyBytes: test.minResponseBodyBytes,
Encodings: defaultSupportedEncodings,
}
handler, err := New(context.Background(), next, cfg, "testing")
require.NoError(t, err) require.NoError(t, err)
rw := httptest.NewRecorder() rw := httptest.NewRecorder()
@ -607,8 +622,11 @@ func Test1xxResponses(t *testing.T) {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
} }
}) })
cfg := dynamic.Compress{
compress, err := New(context.Background(), next, dynamic.Compress{MinResponseBodyBytes: 1024}, "testing") MinResponseBodyBytes: 1024,
Encodings: defaultSupportedEncodings,
}
compress, err := New(context.Background(), next, cfg, "testing")
require.NoError(t, err) require.NoError(t, err)
server := httptest.NewServer(compress) server := httptest.NewServer(compress)

View file

@ -304,7 +304,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client)
InFlightReq: middleware.Spec.InFlightReq, InFlightReq: middleware.Spec.InFlightReq,
Buffering: middleware.Spec.Buffering, Buffering: middleware.Spec.Buffering,
CircuitBreaker: circuitBreaker, CircuitBreaker: circuitBreaker,
Compress: middleware.Spec.Compress, Compress: createCompressMiddleware(middleware.Spec.Compress),
PassTLSClientCert: middleware.Spec.PassTLSClientCert, PassTLSClientCert: middleware.Spec.PassTLSClientCert,
Retry: retry, Retry: retry,
ContentType: middleware.Spec.ContentType, ContentType: middleware.Spec.ContentType,
@ -655,14 +655,49 @@ func createCircuitBreakerMiddleware(circuitBreaker *traefikv1alpha1.CircuitBreak
return cb, nil return cb, nil
} }
func createCompressMiddleware(compress *traefikv1alpha1.Compress) *dynamic.Compress {
if compress == nil {
return nil
}
c := &dynamic.Compress{}
c.SetDefaults()
if compress.ExcludedContentTypes != nil {
c.ExcludedContentTypes = compress.ExcludedContentTypes
}
if compress.IncludedContentTypes != nil {
c.IncludedContentTypes = compress.IncludedContentTypes
}
if compress.MinResponseBodyBytes != nil {
c.MinResponseBodyBytes = *compress.MinResponseBodyBytes
}
if compress.Encodings != nil {
c.Encodings = compress.Encodings
}
if compress.DefaultEncoding != nil {
c.DefaultEncoding = *compress.DefaultEncoding
}
return c
}
func createRateLimitMiddleware(rateLimit *traefikv1alpha1.RateLimit) (*dynamic.RateLimit, error) { func createRateLimitMiddleware(rateLimit *traefikv1alpha1.RateLimit) (*dynamic.RateLimit, error) {
if rateLimit == nil { if rateLimit == nil {
return nil, nil return nil, nil
} }
rl := &dynamic.RateLimit{Average: rateLimit.Average} rl := &dynamic.RateLimit{}
rl.SetDefaults() rl.SetDefaults()
if rateLimit.Average != nil {
rl.Average = *rateLimit.Average
}
if rateLimit.Burst != nil { if rateLimit.Burst != nil {
rl.Burst = *rateLimit.Burst rl.Burst = *rateLimit.Burst
} }

View file

@ -46,7 +46,7 @@ type MiddlewareSpec struct {
InFlightReq *dynamic.InFlightReq `json:"inFlightReq,omitempty"` InFlightReq *dynamic.InFlightReq `json:"inFlightReq,omitempty"`
Buffering *dynamic.Buffering `json:"buffering,omitempty"` Buffering *dynamic.Buffering `json:"buffering,omitempty"`
CircuitBreaker *CircuitBreaker `json:"circuitBreaker,omitempty"` CircuitBreaker *CircuitBreaker `json:"circuitBreaker,omitempty"`
Compress *dynamic.Compress `json:"compress,omitempty"` Compress *Compress `json:"compress,omitempty"`
PassTLSClientCert *dynamic.PassTLSClientCert `json:"passTLSClientCert,omitempty"` PassTLSClientCert *dynamic.PassTLSClientCert `json:"passTLSClientCert,omitempty"`
Retry *Retry `json:"retry,omitempty"` Retry *Retry `json:"retry,omitempty"`
ContentType *dynamic.ContentType `json:"contentType,omitempty"` ContentType *dynamic.ContentType `json:"contentType,omitempty"`
@ -188,7 +188,7 @@ type RateLimit struct {
// It defaults to 0, which means no rate limiting. // It defaults to 0, which means no rate limiting.
// The rate is actually defined by dividing Average by Period. So for a rate below 1req/s, // The rate is actually defined by dividing Average by Period. So for a rate below 1req/s,
// one needs to define a Period larger than a second. // one needs to define a Period larger than a second.
Average int64 `json:"average,omitempty"` Average *int64 `json:"average,omitempty"`
// Period, in combination with Average, defines the actual maximum rate, such as: // Period, in combination with Average, defines the actual maximum rate, such as:
// r = Average / Period. It defaults to a second. // r = Average / Period. It defaults to a second.
Period *intstr.IntOrString `json:"period,omitempty"` Period *intstr.IntOrString `json:"period,omitempty"`
@ -203,6 +203,26 @@ type RateLimit struct {
// +k8s:deepcopy-gen=true // +k8s:deepcopy-gen=true
// Compress holds the compress middleware configuration.
// This middleware compresses responses before sending them to the client, using gzip, brotli, or zstd compression.
// More info: https://doc.traefik.io/traefik/v3.1/middlewares/http/compress/
type Compress struct {
// ExcludedContentTypes defines the list of content types to compare the Content-Type header of the incoming requests and responses before compressing.
// `application/grpc` is always excluded.
ExcludedContentTypes []string `json:"excludedContentTypes,omitempty"`
// IncludedContentTypes defines the list of content types to compare the Content-Type header of the responses before compressing.
IncludedContentTypes []string `json:"includedContentTypes,omitempty"`
// MinResponseBodyBytes defines the minimum amount of bytes a response body must have to be compressed.
// Default: 1024.
MinResponseBodyBytes *int `json:"minResponseBodyBytes,omitempty"`
// Encodings defines the list of supported compression algorithms.
Encodings []string `json:"encodings,omitempty"`
// DefaultEncoding specifies the default encoding if the `Accept-Encoding` header is not in the request or contains a wildcard (`*`).
DefaultEncoding *string `json:"defaultEncoding,omitempty"`
}
// +k8s:deepcopy-gen=true
// Retry holds the retry middleware configuration. // Retry holds the retry middleware configuration.
// This middleware reissues requests a given number of times to a backend server if that server does not reply. // This middleware reissues requests a given number of times to a backend server if that server does not reply.
// As soon as the server answers, the middleware stops retrying, regardless of the response status. // As soon as the server answers, the middleware stops retrying, regardless of the response status.

View file

@ -164,6 +164,47 @@ func (in *ClientTLS) DeepCopy() *ClientTLS {
return out return out
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Compress) DeepCopyInto(out *Compress) {
*out = *in
if in.ExcludedContentTypes != nil {
in, out := &in.ExcludedContentTypes, &out.ExcludedContentTypes
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.IncludedContentTypes != nil {
in, out := &in.IncludedContentTypes, &out.IncludedContentTypes
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.MinResponseBodyBytes != nil {
in, out := &in.MinResponseBodyBytes, &out.MinResponseBodyBytes
*out = new(int)
**out = **in
}
if in.Encodings != nil {
in, out := &in.Encodings, &out.Encodings
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.DefaultEncoding != nil {
in, out := &in.DefaultEncoding, &out.DefaultEncoding
*out = new(string)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Compress.
func (in *Compress) DeepCopy() *Compress {
if in == nil {
return nil
}
out := new(Compress)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DigestAuth) DeepCopyInto(out *DigestAuth) { func (in *DigestAuth) DeepCopyInto(out *DigestAuth) {
*out = *in *out = *in
@ -776,7 +817,7 @@ func (in *MiddlewareSpec) DeepCopyInto(out *MiddlewareSpec) {
} }
if in.Compress != nil { if in.Compress != nil {
in, out := &in.Compress, &out.Compress in, out := &in.Compress, &out.Compress
*out = new(dynamic.Compress) *out = new(Compress)
(*in).DeepCopyInto(*out) (*in).DeepCopyInto(*out)
} }
if in.PassTLSClientCert != nil { if in.PassTLSClientCert != nil {
@ -975,6 +1016,11 @@ func (in *ObjectReference) DeepCopy() *ObjectReference {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RateLimit) DeepCopyInto(out *RateLimit) { func (in *RateLimit) DeepCopyInto(out *RateLimit) {
*out = *in *out = *in
if in.Average != nil {
in, out := &in.Average, &out.Average
*out = new(int64)
**out = **in
}
if in.Period != nil { if in.Period != nil {
in, out := &in.Period, &out.Period in, out := &in.Period, &out.Period
*out = new(intstr.IntOrString) *out = new(intstr.IntOrString)

View file

@ -207,6 +207,7 @@ func Test_buildConfiguration(t *testing.T) {
"traefik/http/middlewares/Middleware02/buffering/retryExpression": "foobar", "traefik/http/middlewares/Middleware02/buffering/retryExpression": "foobar",
"traefik/http/middlewares/Middleware02/buffering/maxRequestBodyBytes": "42", "traefik/http/middlewares/Middleware02/buffering/maxRequestBodyBytes": "42",
"traefik/http/middlewares/Middleware02/buffering/memRequestBodyBytes": "42", "traefik/http/middlewares/Middleware02/buffering/memRequestBodyBytes": "42",
"traefik/http/middlewares/Middleware05/compress/encodings": "foobar, foobar",
"traefik/http/middlewares/Middleware05/compress/minResponseBodyBytes": "42", "traefik/http/middlewares/Middleware05/compress/minResponseBodyBytes": "42",
"traefik/http/middlewares/Middleware18/retry/attempts": "42", "traefik/http/middlewares/Middleware18/retry/attempts": "42",
"traefik/http/middlewares/Middleware19/stripPrefix/prefixes/0": "foobar", "traefik/http/middlewares/Middleware19/stripPrefix/prefixes/0": "foobar",
@ -412,6 +413,10 @@ func Test_buildConfiguration(t *testing.T) {
"Middleware05": { "Middleware05": {
Compress: &dynamic.Compress{ Compress: &dynamic.Compress{
MinResponseBodyBytes: 42, MinResponseBodyBytes: 42,
Encodings: []string{
"foobar",
"foobar",
},
}, },
}, },
"Middleware08": { "Middleware08": {