From 3410541a2fd455ade406a5670ffeffb7b93f7d3f Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Thu, 31 Oct 2019 11:36:05 +0100 Subject: [PATCH] Conditionnal compression based on Content-Type --- docs/content/middlewares/compress.md | 56 +++++++++++++++++++ pkg/config/dynamic/middlewares.go | 4 +- pkg/config/dynamic/zz_generated.deepcopy.go | 7 ++- pkg/middlewares/compress/compress.go | 42 ++++++++++---- pkg/middlewares/compress/compress_test.go | 55 ++++++++++++++---- .../traefik/v1alpha1/zz_generated.deepcopy.go | 2 +- pkg/server/middleware/middlewares.go | 2 +- 7 files changed, 143 insertions(+), 25 deletions(-) diff --git a/docs/content/middlewares/compress.md b/docs/content/middlewares/compress.md index b2e61f185..35282da6c 100644 --- a/docs/content/middlewares/compress.md +++ b/docs/content/middlewares/compress.md @@ -63,3 +63,59 @@ http: * The response body is larger than `1400` bytes. * The `Accept-Encoding` request header contains `gzip`. * 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 +``` diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go index a627ce621..435f1c52d 100644 --- a/pkg/config/dynamic/middlewares.go +++ b/pkg/config/dynamic/middlewares.go @@ -92,7 +92,9 @@ type CircuitBreaker struct { // +k8s:deepcopy-gen=true // 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 diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index 5e3285895..8f885904b 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -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. 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) + } return } @@ -662,7 +667,7 @@ func (in *Middleware) DeepCopyInto(out *Middleware) { if in.Compress != nil { in, out := &in.Compress, &out.Compress *out = new(Compress) - **out = **in + (*in).DeepCopyInto(*out) } if in.PassTLSClientCert != nil { in, out := &in.PassTLSClientCert, &out.PassTLSClientCert diff --git a/pkg/middlewares/compress/compress.go b/pkg/middlewares/compress/compress.go index 7269d2c39..ea254fe48 100644 --- a/pkg/middlewares/compress/compress.go +++ b/pkg/middlewares/compress/compress.go @@ -3,10 +3,11 @@ package compress import ( "compress/gzip" "context" + "mime" "net/http" - "strings" "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/middlewares" "github.com/containous/traefik/v2/pkg/tracing" @@ -19,23 +20,35 @@ const ( // Compress is a middleware that allows to compress the response. type compress struct { - next http.Handler - name string + next http.Handler + name string + excludes []string } // 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") - return &compress{ - next: next, - name: name, - }, nil + excludes := []string{"application/grpc"} + for _, v := range conf.ExcludedContentTypes { + mediaType, _, err := mime.ParseMediaType(v) + 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) { - contentType := req.Header.Get("Content-Type") - if strings.HasPrefix(contentType, "application/grpc") { + mediaType, _, err := mime.ParseMediaType(req.Header.Get("Content-Type")) + if err != nil { + log.FromContext(middlewares.GetLoggerCtx(context.Background(), c.name, typeName)).Debug(err) + } + + if contains(c.excludes, mediaType) { c.next.ServeHTTP(rw, req) } else { 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) } + +func contains(values []string, val string) bool { + for _, v := range values { + if v == val { + return true + } + } + return false +} diff --git a/pkg/middlewares/compress/compress_test.go b/pkg/middlewares/compress/compress_test.go index bca126916..3c1ac348a 100644 --- a/pkg/middlewares/compress/compress_test.go +++ b/pkg/middlewares/compress/compress_test.go @@ -1,12 +1,14 @@ package compress import ( + "context" "io/ioutil" "net/http" "net/http/httptest" "testing" "github.com/NYTimes/gziphandler" + "github.com/containous/traefik/v2/pkg/config/dynamic" "github.com/containous/traefik/v2/pkg/testhelpers" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -86,26 +88,57 @@ func TestShouldNotCompressWhenNoAcceptEncodingHeader(t *testing.T) { assert.EqualValues(t, rw.Body.Bytes(), fakeBody) } -func TestShouldNotCompressWhenGRPC(t *testing.T) { - req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil) - req.Header.Add(acceptEncodingHeader, gzipValue) - req.Header.Add(contentTypeHeader, "application/grpc") - +func TestShouldNotCompressWhenSpecificContentType(t *testing.T) { baseBody := generateBytes(gziphandler.DefaultMinSize) + next := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { _, err := rw.Write(baseBody) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) } }) - handler := &compress{next: next} - rw := httptest.NewRecorder() - handler.ServeHTTP(rw, req) + 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", + }, + } - assert.Empty(t, rw.Header().Get(acceptEncodingHeader)) - assert.Empty(t, rw.Header().Get(contentEncodingHeader)) - assert.EqualValues(t, rw.Body.Bytes(), baseBody) + 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() + handler.ServeHTTP(rw, req) + + assert.Empty(t, rw.Header().Get(acceptEncodingHeader)) + assert.Empty(t, rw.Header().Get(contentEncodingHeader)) + assert.EqualValues(t, rw.Body.Bytes(), baseBody) + }) + } } func TestIntegrationShouldNotCompress(t *testing.T) { diff --git a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go index 3f6770cbe..8f254d622 100644 --- a/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/provider/kubernetes/crd/traefik/v1alpha1/zz_generated.deepcopy.go @@ -553,7 +553,7 @@ func (in *MiddlewareSpec) DeepCopyInto(out *MiddlewareSpec) { if in.Compress != nil { in, out := &in.Compress, &out.Compress *out = new(dynamic.Compress) - **out = **in + (*in).DeepCopyInto(*out) } if in.PassTLSClientCert != nil { in, out := &in.PassTLSClientCert, &out.PassTLSClientCert diff --git a/pkg/server/middleware/middlewares.go b/pkg/server/middleware/middlewares.go index e38a97e92..16b5fe444 100644 --- a/pkg/server/middleware/middlewares.go +++ b/pkg/server/middleware/middlewares.go @@ -168,7 +168,7 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) ( return nil, badConf } middleware = func(next http.Handler) (http.Handler, error) { - return compress.New(ctx, next, middlewareName) + return compress.New(ctx, next, *config.Compress, middlewareName) } }