Conditionnal compression based on Content-Type

This commit is contained in:
Ludovic Fernandez 2019-10-31 11:36:05 +01:00 committed by Traefiker Bot
parent 1f39083555
commit 3410541a2f
7 changed files with 143 additions and 25 deletions

View file

@ -63,3 +63,59 @@ http:
* The response body is larger than `1400` bytes. * The response body is larger than `1400` bytes.
* The `Accept-Encoding` request header contains `gzip`. * The `Accept-Encoding` request header contains `gzip`.
* The response is not already compressed, i.e. the `Content-Encoding` response header is not already set. * The response is not already compressed, i.e. the `Content-Encoding` response header is not already set.
## Configuration Options
### `excludedContentTypes`
`excludedContentTypes` specifies a list of content types to compare the `Content-Type` header of the incoming requests to before compressing.
The requests with content types defined in `excludedContentTypes` are not compressed.
Content types are compared in a case-insensitive, whitespace-ignored manner.
```yaml tab="Docker"
labels:
- "traefik.http.middlewares.test-compress.compress.excludedcontenttypes=text/event-stream"
```
```yaml tab="Kubernetes"
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: test-compress
spec:
compress:
excludedContentTypes:
- text/event-stream
```
```yaml tab="Consul Catalog"
- "traefik.http.middlewares.test-compress.compress.excludedcontenttypes=text/event-stream"
```
```json tab="Marathon"
"labels": {
"traefik.http.middlewares.test-compress.compress.excludedcontenttypes": "text/event-stream"
}
```
```yaml tab="Rancher"
labels:
- "traefik.http.middlewares.test-compress.compress.excludedcontenttypes=text/event-stream"
```
```toml tab="File (TOML)"
[http.middlewares]
[http.middlewares.test-compress.compress]
excludedContentTypes = ["text/event-stream"]
```
```yaml tab="File (YAML)"
http:
middlewares:
test-compress:
compress:
excludedContentTypes:
- text/event-stream
```

View file

@ -92,7 +92,9 @@ type CircuitBreaker struct {
// +k8s:deepcopy-gen=true // +k8s:deepcopy-gen=true
// Compress holds the compress configuration. // Compress holds the compress configuration.
type Compress struct{} type Compress struct {
ExcludedContentTypes []string `json:"excludedContentTypes,omitempty" toml:"excludedContentTypes,omitempty" yaml:"excludedContentTypes,omitempty" export:"true"`
}
// +k8s:deepcopy-gen=true // +k8s:deepcopy-gen=true

View file

@ -173,6 +173,11 @@ func (in *ClientTLS) DeepCopy() *ClientTLS {
// 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 *Compress) DeepCopyInto(out *Compress) { func (in *Compress) DeepCopyInto(out *Compress) {
*out = *in *out = *in
if in.ExcludedContentTypes != nil {
in, out := &in.ExcludedContentTypes, &out.ExcludedContentTypes
*out = make([]string, len(*in))
copy(*out, *in)
}
return return
} }
@ -662,7 +667,7 @@ func (in *Middleware) DeepCopyInto(out *Middleware) {
if in.Compress != nil { if in.Compress != nil {
in, out := &in.Compress, &out.Compress in, out := &in.Compress, &out.Compress
*out = new(Compress) *out = new(Compress)
**out = **in (*in).DeepCopyInto(*out)
} }
if in.PassTLSClientCert != nil { if in.PassTLSClientCert != nil {
in, out := &in.PassTLSClientCert, &out.PassTLSClientCert in, out := &in.PassTLSClientCert, &out.PassTLSClientCert

View file

@ -3,10 +3,11 @@ package compress
import ( import (
"compress/gzip" "compress/gzip"
"context" "context"
"mime"
"net/http" "net/http"
"strings"
"github.com/NYTimes/gziphandler" "github.com/NYTimes/gziphandler"
"github.com/containous/traefik/v2/pkg/config/dynamic"
"github.com/containous/traefik/v2/pkg/log" "github.com/containous/traefik/v2/pkg/log"
"github.com/containous/traefik/v2/pkg/middlewares" "github.com/containous/traefik/v2/pkg/middlewares"
"github.com/containous/traefik/v2/pkg/tracing" "github.com/containous/traefik/v2/pkg/tracing"
@ -21,21 +22,33 @@ const (
type compress struct { type compress struct {
next http.Handler next http.Handler
name string name string
excludes []string
} }
// New creates a new compress middleware. // New creates a new compress middleware.
func New(ctx context.Context, next http.Handler, name string) (http.Handler, error) { func New(ctx context.Context, next http.Handler, conf dynamic.Compress, name string) (http.Handler, error) {
log.FromContext(middlewares.GetLoggerCtx(ctx, name, typeName)).Debug("Creating middleware") log.FromContext(middlewares.GetLoggerCtx(ctx, name, typeName)).Debug("Creating middleware")
return &compress{ excludes := []string{"application/grpc"}
next: next, for _, v := range conf.ExcludedContentTypes {
name: name, mediaType, _, err := mime.ParseMediaType(v)
}, nil if err != nil {
return nil, err
}
excludes = append(excludes, mediaType)
}
return &compress{next: next, name: name, excludes: excludes}, nil
} }
func (c *compress) ServeHTTP(rw http.ResponseWriter, req *http.Request) { func (c *compress) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
contentType := req.Header.Get("Content-Type") mediaType, _, err := mime.ParseMediaType(req.Header.Get("Content-Type"))
if strings.HasPrefix(contentType, "application/grpc") { if err != nil {
log.FromContext(middlewares.GetLoggerCtx(context.Background(), c.name, typeName)).Debug(err)
}
if contains(c.excludes, mediaType) {
c.next.ServeHTTP(rw, req) c.next.ServeHTTP(rw, req)
} else { } else {
ctx := middlewares.GetLoggerCtx(req.Context(), c.name, typeName) ctx := middlewares.GetLoggerCtx(req.Context(), c.name, typeName)
@ -57,3 +70,12 @@ func gzipHandler(ctx context.Context, h http.Handler) http.Handler {
return wrapper(h) return wrapper(h)
} }
func contains(values []string, val string) bool {
for _, v := range values {
if v == val {
return true
}
}
return false
}

View file

@ -1,12 +1,14 @@
package compress package compress
import ( import (
"context"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/NYTimes/gziphandler" "github.com/NYTimes/gziphandler"
"github.com/containous/traefik/v2/pkg/config/dynamic"
"github.com/containous/traefik/v2/pkg/testhelpers" "github.com/containous/traefik/v2/pkg/testhelpers"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -86,19 +88,48 @@ func TestShouldNotCompressWhenNoAcceptEncodingHeader(t *testing.T) {
assert.EqualValues(t, rw.Body.Bytes(), fakeBody) assert.EqualValues(t, rw.Body.Bytes(), fakeBody)
} }
func TestShouldNotCompressWhenGRPC(t *testing.T) { func TestShouldNotCompressWhenSpecificContentType(t *testing.T) {
req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil)
req.Header.Add(acceptEncodingHeader, gzipValue)
req.Header.Add(contentTypeHeader, "application/grpc")
baseBody := generateBytes(gziphandler.DefaultMinSize) baseBody := generateBytes(gziphandler.DefaultMinSize)
next := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { next := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
_, err := rw.Write(baseBody) _, err := rw.Write(baseBody)
if err != nil { if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError) http.Error(rw, err.Error(), http.StatusInternalServerError)
} }
}) })
handler := &compress{next: next}
testCases := []struct {
desc string
conf dynamic.Compress
reqContentType string
}{
{
desc: "text/event-stream",
conf: dynamic.Compress{
ExcludedContentTypes: []string{"text/event-stream"},
},
reqContentType: "text/event-stream",
},
{
desc: "application/grpc",
conf: dynamic.Compress{},
reqContentType: "application/grpc",
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil)
req.Header.Add(acceptEncodingHeader, gzipValue)
if test.reqContentType != "" {
req.Header.Add(contentTypeHeader, test.reqContentType)
}
handler, err := New(context.Background(), next, test.conf, "test")
require.NoError(t, err)
rw := httptest.NewRecorder() rw := httptest.NewRecorder()
handler.ServeHTTP(rw, req) handler.ServeHTTP(rw, req)
@ -106,6 +137,8 @@ func TestShouldNotCompressWhenGRPC(t *testing.T) {
assert.Empty(t, rw.Header().Get(acceptEncodingHeader)) assert.Empty(t, rw.Header().Get(acceptEncodingHeader))
assert.Empty(t, rw.Header().Get(contentEncodingHeader)) assert.Empty(t, rw.Header().Get(contentEncodingHeader))
assert.EqualValues(t, rw.Body.Bytes(), baseBody) assert.EqualValues(t, rw.Body.Bytes(), baseBody)
})
}
} }
func TestIntegrationShouldNotCompress(t *testing.T) { func TestIntegrationShouldNotCompress(t *testing.T) {

View file

@ -553,7 +553,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(dynamic.Compress)
**out = **in (*in).DeepCopyInto(*out)
} }
if in.PassTLSClientCert != nil { if in.PassTLSClientCert != nil {
in, out := &in.PassTLSClientCert, &out.PassTLSClientCert in, out := &in.PassTLSClientCert, &out.PassTLSClientCert

View file

@ -168,7 +168,7 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) (
return nil, badConf return nil, badConf
} }
middleware = func(next http.Handler) (http.Handler, error) { middleware = func(next http.Handler) (http.Handler, error) {
return compress.New(ctx, next, middlewareName) return compress.New(ctx, next, *config.Compress, middlewareName)
} }
} }