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 `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
```

View file

@ -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

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.
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

View file

@ -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
}

View file

@ -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) {

View file

@ -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

View file

@ -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)
}
}