Add encodings option to the compression middleware
This commit is contained in:
parent
b611f967b7
commit
75881359ab
19 changed files with 389 additions and 92 deletions
|
@ -255,3 +255,48 @@ http:
|
|||
[http.middlewares.test-compress.compress]
|
||||
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"]
|
||||
```
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
- "traefik.http.middlewares.middleware05.circuitbreaker.responsecode=42"
|
||||
- "traefik.http.middlewares.middleware06.compress=true"
|
||||
- "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.includedcontenttypes=foobar, foobar"
|
||||
- "traefik.http.middlewares.middleware06.compress.minresponsebodybytes=42"
|
||||
|
|
|
@ -143,6 +143,7 @@
|
|||
excludedContentTypes = ["foobar", "foobar"]
|
||||
includedContentTypes = ["foobar", "foobar"]
|
||||
minResponseBodyBytes = 42
|
||||
encodings = ["foobar", "foobar"]
|
||||
defaultEncoding = "foobar"
|
||||
[http.middlewares.Middleware07]
|
||||
[http.middlewares.Middleware07.contentType]
|
||||
|
|
|
@ -152,6 +152,9 @@ http:
|
|||
- foobar
|
||||
- foobar
|
||||
minResponseBodyBytes: 42
|
||||
encodings:
|
||||
- foobar
|
||||
- foobar
|
||||
defaultEncoding: foobar
|
||||
Middleware07:
|
||||
contentType:
|
||||
|
|
|
@ -904,7 +904,7 @@ spec:
|
|||
compress:
|
||||
description: |-
|
||||
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/
|
||||
properties:
|
||||
defaultEncoding:
|
||||
|
@ -912,6 +912,12 @@ spec:
|
|||
the `Accept-Encoding` header is not in the request or contains
|
||||
a wildcard (`*`).
|
||||
type: string
|
||||
encodings:
|
||||
description: Encodings defines the list of supported compression
|
||||
algorithms.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
excludedContentTypes:
|
||||
description: |-
|
||||
ExcludedContentTypes defines the list of content types to compare the Content-Type header of the incoming requests and responses before compressing.
|
||||
|
|
|
@ -22,6 +22,8 @@ THIS FILE MUST NOT BE EDITED BY HAND
|
|||
| `traefik/http/middlewares/Middleware05/circuitBreaker/recoveryDuration` | `42s` |
|
||||
| `traefik/http/middlewares/Middleware05/circuitBreaker/responseCode` | `42` |
|
||||
| `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/1` | `foobar` |
|
||||
| `traefik/http/middlewares/Middleware06/compress/includedContentTypes/0` | `foobar` |
|
||||
|
|
|
@ -180,7 +180,7 @@ spec:
|
|||
compress:
|
||||
description: |-
|
||||
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/
|
||||
properties:
|
||||
defaultEncoding:
|
||||
|
@ -188,6 +188,12 @@ spec:
|
|||
the `Accept-Encoding` header is not in the request or contains
|
||||
a wildcard (`*`).
|
||||
type: string
|
||||
encodings:
|
||||
description: Encodings defines the list of supported compression
|
||||
algorithms.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
excludedContentTypes:
|
||||
description: |-
|
||||
ExcludedContentTypes defines the list of content types to compare the Content-Type header of the incoming requests and responses before compressing.
|
||||
|
|
|
@ -904,7 +904,7 @@ spec:
|
|||
compress:
|
||||
description: |-
|
||||
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/
|
||||
properties:
|
||||
defaultEncoding:
|
||||
|
@ -912,6 +912,12 @@ spec:
|
|||
the `Accept-Encoding` header is not in the request or contains
|
||||
a wildcard (`*`).
|
||||
type: string
|
||||
encodings:
|
||||
description: Encodings defines the list of supported compression
|
||||
algorithms.
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
excludedContentTypes:
|
||||
description: |-
|
||||
ExcludedContentTypes defines the list of content types to compare the Content-Type header of the incoming requests and responses before compressing.
|
||||
|
|
|
@ -165,8 +165,7 @@ func (c *CircuitBreaker) SetDefaults() {
|
|||
// +k8s:deepcopy-gen=true
|
||||
|
||||
// Compress holds the compress middleware configuration.
|
||||
// This middleware compresses responses before sending them to the client, using gzip compression.
|
||||
// More info: https://doc.traefik.io/traefik/v3.1/middlewares/http/compress/
|
||||
// This middleware compresses responses before sending them to the client, using gzip, brotli, or zstd compression.
|
||||
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.
|
||||
|
@ -176,10 +175,16 @@ type Compress struct {
|
|||
// MinResponseBodyBytes defines the minimum amount of bytes a response body must have to be compressed.
|
||||
// Default: 1024.
|
||||
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 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
|
||||
|
||||
// DigestAuth holds the digest auth middleware configuration.
|
||||
|
|
|
@ -158,6 +158,11 @@ func (in *Compress) DeepCopyInto(out *Compress) {
|
|||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Encodings != nil {
|
||||
in, out := &in.Encodings, &out.Encodings
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -137,6 +137,7 @@ func TestDecodeConfiguration(t *testing.T) {
|
|||
"traefik.http.middlewares.Middleware17.stripprefix.prefixes": "foobar, fiibar",
|
||||
"traefik.http.middlewares.Middleware17.stripprefix.forceslash": "true",
|
||||
"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.Middleware20.plugin.tomato.aaa": "foo1",
|
||||
"traefik.http.middlewares.Middleware20.plugin.tomato.bbb": "foo2",
|
||||
|
@ -493,6 +494,10 @@ func TestDecodeConfiguration(t *testing.T) {
|
|||
"Middleware19": {
|
||||
Compress: &dynamic.Compress{
|
||||
MinResponseBodyBytes: 42,
|
||||
Encodings: []string{
|
||||
"foobar",
|
||||
"fiibar",
|
||||
},
|
||||
},
|
||||
},
|
||||
"Middleware2": {
|
||||
|
@ -1009,6 +1014,10 @@ func TestEncodeConfiguration(t *testing.T) {
|
|||
"Middleware19": {
|
||||
Compress: &dynamic.Compress{
|
||||
MinResponseBodyBytes: 42,
|
||||
Encodings: []string{
|
||||
"foobar",
|
||||
"fiibar",
|
||||
},
|
||||
},
|
||||
},
|
||||
"Middleware2": {
|
||||
|
@ -1377,6 +1386,7 @@ func TestEncodeConfiguration(t *testing.T) {
|
|||
"traefik.HTTP.Middlewares.Middleware17.StripPrefix.Prefixes": "foobar, fiibar",
|
||||
"traefik.HTTP.Middlewares.Middleware17.StripPrefix.ForceSlash": "true",
|
||||
"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.Middleware20.Plugin.tomato.aaa": "foo1",
|
||||
"traefik.HTTP.Middlewares.Middleware20.Plugin.tomato.bbb": "foo2",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -304,7 +304,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client)
|
|||
InFlightReq: middleware.Spec.InFlightReq,
|
||||
Buffering: middleware.Spec.Buffering,
|
||||
CircuitBreaker: circuitBreaker,
|
||||
Compress: middleware.Spec.Compress,
|
||||
Compress: createCompressMiddleware(middleware.Spec.Compress),
|
||||
PassTLSClientCert: middleware.Spec.PassTLSClientCert,
|
||||
Retry: retry,
|
||||
ContentType: middleware.Spec.ContentType,
|
||||
|
@ -655,14 +655,49 @@ func createCircuitBreakerMiddleware(circuitBreaker *traefikv1alpha1.CircuitBreak
|
|||
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) {
|
||||
if rateLimit == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rl := &dynamic.RateLimit{Average: rateLimit.Average}
|
||||
rl := &dynamic.RateLimit{}
|
||||
rl.SetDefaults()
|
||||
|
||||
if rateLimit.Average != nil {
|
||||
rl.Average = *rateLimit.Average
|
||||
}
|
||||
|
||||
if rateLimit.Burst != nil {
|
||||
rl.Burst = *rateLimit.Burst
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ type MiddlewareSpec struct {
|
|||
InFlightReq *dynamic.InFlightReq `json:"inFlightReq,omitempty"`
|
||||
Buffering *dynamic.Buffering `json:"buffering,omitempty"`
|
||||
CircuitBreaker *CircuitBreaker `json:"circuitBreaker,omitempty"`
|
||||
Compress *dynamic.Compress `json:"compress,omitempty"`
|
||||
Compress *Compress `json:"compress,omitempty"`
|
||||
PassTLSClientCert *dynamic.PassTLSClientCert `json:"passTLSClientCert,omitempty"`
|
||||
Retry *Retry `json:"retry,omitempty"`
|
||||
ContentType *dynamic.ContentType `json:"contentType,omitempty"`
|
||||
|
@ -188,7 +188,7 @@ type RateLimit struct {
|
|||
// 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,
|
||||
// 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:
|
||||
// r = Average / Period. It defaults to a second.
|
||||
Period *intstr.IntOrString `json:"period,omitempty"`
|
||||
|
@ -203,6 +203,26 @@ type RateLimit struct {
|
|||
|
||||
// +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.
|
||||
// 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.
|
||||
|
|
|
@ -164,6 +164,47 @@ func (in *ClientTLS) DeepCopy() *ClientTLS {
|
|||
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.
|
||||
func (in *DigestAuth) DeepCopyInto(out *DigestAuth) {
|
||||
*out = *in
|
||||
|
@ -776,7 +817,7 @@ func (in *MiddlewareSpec) DeepCopyInto(out *MiddlewareSpec) {
|
|||
}
|
||||
if in.Compress != nil {
|
||||
in, out := &in.Compress, &out.Compress
|
||||
*out = new(dynamic.Compress)
|
||||
*out = new(Compress)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
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.
|
||||
func (in *RateLimit) DeepCopyInto(out *RateLimit) {
|
||||
*out = *in
|
||||
if in.Average != nil {
|
||||
in, out := &in.Average, &out.Average
|
||||
*out = new(int64)
|
||||
**out = **in
|
||||
}
|
||||
if in.Period != nil {
|
||||
in, out := &in.Period, &out.Period
|
||||
*out = new(intstr.IntOrString)
|
||||
|
|
|
@ -207,6 +207,7 @@ func Test_buildConfiguration(t *testing.T) {
|
|||
"traefik/http/middlewares/Middleware02/buffering/retryExpression": "foobar",
|
||||
"traefik/http/middlewares/Middleware02/buffering/maxRequestBodyBytes": "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/Middleware18/retry/attempts": "42",
|
||||
"traefik/http/middlewares/Middleware19/stripPrefix/prefixes/0": "foobar",
|
||||
|
@ -412,6 +413,10 @@ func Test_buildConfiguration(t *testing.T) {
|
|||
"Middleware05": {
|
||||
Compress: &dynamic.Compress{
|
||||
MinResponseBodyBytes: 42,
|
||||
Encodings: []string{
|
||||
"foobar",
|
||||
"foobar",
|
||||
},
|
||||
},
|
||||
},
|
||||
"Middleware08": {
|
||||
|
|
Loading…
Reference in a new issue