Skip to content

Commit

Permalink
Conditionnal compression based on Content-Type
Browse files Browse the repository at this point in the history
  • Loading branch information
ldez authored and traefiker committed Oct 31, 2019
1 parent 1f39083 commit 3410541
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 25 deletions.
56 changes: 56 additions & 0 deletions docs/content/middlewares/compress.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
4 changes: 3 additions & 1 deletion pkg/config/dynamic/middlewares.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 6 additions & 1 deletion pkg/config/dynamic/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 32 additions & 10 deletions pkg/middlewares/compress/compress.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand All @@ -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
}
55 changes: 44 additions & 11 deletions pkg/middlewares/compress/compress_test.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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) {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pkg/server/middleware/middlewares.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down

0 comments on commit 3410541

Please sign in to comment.