compress: preserve status code

This commit is contained in:
Ludovic Fernandez 2017-08-21 11:10:03 +02:00 committed by Traefiker
parent ec3e2c08b8
commit 5313922bb7
6 changed files with 189 additions and 56 deletions

4
glide.lock generated
View file

@ -1,4 +1,4 @@
hash: c7f74f8b5eac556a531d2cb3bae212e32785c1128d416c75fabedc31d27f0e03 hash: b056b388f961ddc3509b19b4546d8dfc70fd50ebd1ca47f2fd2de67bf49ba01e
updated: 2017-08-12T14:15:06.346751095+02:00 updated: 2017-08-12T14:15:06.346751095+02:00
imports: imports:
- name: cloud.google.com/go - name: cloud.google.com/go
@ -377,7 +377,7 @@ imports:
repo: https://github.com/ijc25/Gotty.git repo: https://github.com/ijc25/Gotty.git
vcs: git vcs: git
- name: github.com/NYTimes/gziphandler - name: github.com/NYTimes/gziphandler
version: 316adfc72ed3b0157975917adf62ba2dc31842ce version: 824b33f2a7457025697878c865c323f801118043
repo: https://github.com/containous/gziphandler.git repo: https://github.com/containous/gziphandler.git
vcs: git vcs: git
- name: github.com/ogier/pflag - name: github.com/ogier/pflag

View file

@ -81,6 +81,7 @@ import:
- package: github.com/NYTimes/gziphandler - package: github.com/NYTimes/gziphandler
repo: https://github.com/containous/gziphandler.git repo: https://github.com/containous/gziphandler.git
vcs: git vcs: git
version: ^v1002.0.0
- package: github.com/docker/leadership - package: github.com/docker/leadership
- package: github.com/satori/go.uuid - package: github.com/satori/go.uuid
version: ^1.1.0 version: ^1.1.0

View file

@ -17,7 +17,10 @@ func (c *Compress) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.
} }
func gzipHandler(h http.Handler) http.Handler { func gzipHandler(h http.Handler) http.Handler {
wrapper, err := gziphandler.NewGzipHandler(gzip.DefaultCompression, gziphandler.DefaultMinSize, &gziphandler.GzipResponseWriterWrapper{}) wrapper, err := gziphandler.GzipHandlerWithOpts(
&gziphandler.GzipResponseWriterWrapper{},
gziphandler.CompressionLevel(gzip.DefaultCompression),
gziphandler.MinSize(gziphandler.DefaultMinSize))
if err != nil { if err != nil {
log.Error(err) log.Error(err)
} }

View file

@ -9,6 +9,7 @@ import (
"github.com/NYTimes/gziphandler" "github.com/NYTimes/gziphandler"
"github.com/containous/traefik/testhelpers" "github.com/containous/traefik/testhelpers"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/negroni" "github.com/urfave/negroni"
) )
@ -80,63 +81,114 @@ func TestShouldNotCompressWhenNoAcceptEncodingHeader(t *testing.T) {
assert.EqualValues(t, rw.Body.Bytes(), fakeBody) assert.EqualValues(t, rw.Body.Bytes(), fakeBody)
} }
func TestIntegrationShouldNotCompressWhenContentAlreadyCompressed(t *testing.T) { func TestIntegrationShouldNotCompress(t *testing.T) {
fakeCompressedBody := generateBytes(100000) fakeCompressedBody := generateBytes(100000)
handler := func(rw http.ResponseWriter, r *http.Request) {
rw.Header().Add(contentEncodingHeader, gzipValue)
rw.Header().Add(varyHeader, acceptEncodingHeader)
rw.Write(fakeCompressedBody)
}
comp := &Compress{} comp := &Compress{}
negro := negroni.New(comp) testCases := []struct {
negro.UseHandlerFunc(handler) name string
ts := httptest.NewServer(negro) handler func(rw http.ResponseWriter, r *http.Request)
defer ts.Close() expectedStatusCode int
}{
{
name: "when content already compressed",
handler: func(rw http.ResponseWriter, r *http.Request) {
rw.Header().Add(contentEncodingHeader, gzipValue)
rw.Header().Add(varyHeader, acceptEncodingHeader)
rw.Write(fakeCompressedBody)
},
expectedStatusCode: http.StatusOK,
},
{
name: "when content already compressed and status code Created",
handler: func(rw http.ResponseWriter, r *http.Request) {
rw.Header().Add(contentEncodingHeader, gzipValue)
rw.Header().Add(varyHeader, acceptEncodingHeader)
rw.WriteHeader(http.StatusCreated)
rw.Write(fakeCompressedBody)
},
expectedStatusCode: http.StatusCreated,
},
}
client := &http.Client{} for _, test := range testCases {
req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil)
req.Header.Add(acceptEncodingHeader, gzipValue)
resp, err := client.Do(req) t.Run(test.name, func(t *testing.T) {
assert.NoError(t, err, "there should be no error") negro := negroni.New(comp)
negro.UseHandlerFunc(test.handler)
ts := httptest.NewServer(negro)
defer ts.Close()
assert.Equal(t, gzipValue, resp.Header.Get(contentEncodingHeader)) req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil)
assert.Equal(t, acceptEncodingHeader, resp.Header.Get(varyHeader)) req.Header.Add(acceptEncodingHeader, gzipValue)
body, err := ioutil.ReadAll(resp.Body) resp, err := http.DefaultClient.Do(req)
assert.EqualValues(t, fakeCompressedBody, body) require.NoError(t, err)
assert.Equal(t, test.expectedStatusCode, resp.StatusCode)
assert.Equal(t, gzipValue, resp.Header.Get(contentEncodingHeader))
assert.Equal(t, acceptEncodingHeader, resp.Header.Get(varyHeader))
body, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
assert.EqualValues(t, fakeCompressedBody, body)
})
}
} }
func TestIntegrationShouldCompressWhenAcceptEncodingHeaderIsPresent(t *testing.T) { func TestIntegrationShouldCompress(t *testing.T) {
fakeBody := generateBytes(100000) fakeBody := generateBytes(100000)
handler := func(rw http.ResponseWriter, r *http.Request) { testCases := []struct {
rw.Write(fakeBody) name string
handler func(rw http.ResponseWriter, r *http.Request)
expectedStatusCode int
}{
{
name: "when AcceptEncoding header is present",
handler: func(rw http.ResponseWriter, r *http.Request) {
rw.Write(fakeBody)
},
expectedStatusCode: http.StatusOK,
},
{
name: "when AcceptEncoding header is present and status code Created",
handler: func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusCreated)
rw.Write(fakeBody)
},
expectedStatusCode: http.StatusCreated,
},
} }
comp := &Compress{} for _, test := range testCases {
negro := negroni.New(comp) t.Run(test.name, func(t *testing.T) {
negro.UseHandlerFunc(handler) comp := &Compress{}
ts := httptest.NewServer(negro)
defer ts.Close()
client := &http.Client{} negro := negroni.New(comp)
req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil) negro.UseHandlerFunc(test.handler)
req.Header.Add(acceptEncodingHeader, gzipValue) ts := httptest.NewServer(negro)
defer ts.Close()
resp, err := client.Do(req) req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil)
assert.NoError(t, err, "there should be no error") req.Header.Add(acceptEncodingHeader, gzipValue)
assert.Equal(t, gzipValue, resp.Header.Get(contentEncodingHeader)) resp, err := http.DefaultClient.Do(req)
assert.Equal(t, acceptEncodingHeader, resp.Header.Get(varyHeader)) require.NoError(t, err)
body, err := ioutil.ReadAll(resp.Body) assert.Equal(t, test.expectedStatusCode, resp.StatusCode)
if assert.ObjectsAreEqualValues(body, fakeBody) {
assert.Fail(t, "expected a compressed body", "got %v", body) assert.Equal(t, gzipValue, resp.Header.Get(contentEncodingHeader))
assert.Equal(t, acceptEncodingHeader, resp.Header.Get(varyHeader))
body, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
if assert.ObjectsAreEqualValues(body, fakeBody) {
assert.Fail(t, "expected a compressed body", "got %v", body)
}
})
} }
} }

View file

@ -81,6 +81,8 @@ type GzipResponseWriter struct {
minSize int // Specifed the minimum response size to gzip. If the response length is bigger than this value, it is compressed. minSize int // Specifed the minimum response size to gzip. If the response length is bigger than this value, it is compressed.
buf []byte // Holds the first part of the write before reaching the minSize or the end of the write. buf []byte // Holds the first part of the write before reaching the minSize or the end of the write.
contentTypes []string // Only compress if the response is one of these content-types. All are accepted if empty.
} }
// Write appends data to the gzip writer. // Write appends data to the gzip writer.
@ -101,8 +103,10 @@ func (w *GzipResponseWriter) Write(b []byte) (int, error) {
// On the first write, w.buf changes from nil to a valid slice // On the first write, w.buf changes from nil to a valid slice
w.buf = append(w.buf, b...) w.buf = append(w.buf, b...)
// If the global writes are bigger than the minSize, compression is enable. // If the global writes are bigger than the minSize and we're about to write
if len(w.buf) >= w.minSize { // a response containing a content type we want to handle, enable
// compression.
if len(w.buf) >= w.minSize && handleContentType(w.contentTypes, w) {
err := w.startGzip() err := w.startGzip()
if err != nil { if err != nil {
return 0, err return 0, err
@ -231,24 +235,29 @@ func NewGzipLevelHandler(level int) (func(http.Handler) http.Handler, error) {
// NewGzipLevelAndMinSize behave as NewGzipLevelHandler except it let the caller // NewGzipLevelAndMinSize behave as NewGzipLevelHandler except it let the caller
// specify the minimum size before compression. // specify the minimum size before compression.
func NewGzipLevelAndMinSize(level, minSize int) (func(http.Handler) http.Handler, error) { func NewGzipLevelAndMinSize(level, minSize int) (func(http.Handler) http.Handler, error) {
return NewGzipHandler(level, minSize, &GzipResponseWriter{}) return GzipHandlerWithOpts(&GzipResponseWriter{}, CompressionLevel(level), MinSize(minSize))
} }
// NewGzipHandler behave as NewGzipLevelHandler except it let the caller func GzipHandlerWithOpts(gw GzipWriter, opts ...option) (func(http.Handler) http.Handler, error) {
// specify the minimum size before compression and a GzipWriter.
func NewGzipHandler(level, minSize int, gw GzipWriter) (func(http.Handler) http.Handler, error) {
if level != gzip.DefaultCompression && (level < gzip.BestSpeed || level > gzip.BestCompression) {
return nil, fmt.Errorf("invalid compression level requested: %d", level)
}
if minSize < 0 {
return nil, errors.New("minimum size must be more than zero")
}
if gw == nil { if gw == nil {
return nil, errors.New("the GzipWriter must be defined") return nil, errors.New("the GzipWriter must be defined")
} }
c := &config{
level: gzip.DefaultCompression,
minSize: DefaultMinSize,
}
for _, o := range opts {
o(c)
}
if err := c.validate(); err != nil {
return nil, err
}
return func(h http.Handler) http.Handler { return func(h http.Handler) http.Handler {
index := poolIndex(level) index := poolIndex(c.level)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add(vary, acceptEncoding) w.Header().Add(vary, acceptEncoding)
@ -256,7 +265,8 @@ func NewGzipHandler(level, minSize int, gw GzipWriter) (func(http.Handler) http.
if acceptsGzip(r) { if acceptsGzip(r) {
gw.SetResponseWriter(w) gw.SetResponseWriter(w)
gw.setIndex(index) gw.setIndex(index)
gw.setMinSize(minSize) gw.setMinSize(c.minSize)
gw.setContentTypes(c.contentTypes)
defer gw.Close() defer gw.Close()
h.ServeHTTP(gw, r) h.ServeHTTP(gw, r)
@ -267,6 +277,48 @@ func NewGzipHandler(level, minSize int, gw GzipWriter) (func(http.Handler) http.
}, nil }, nil
} }
// Used for functional configuration.
type config struct {
minSize int
level int
contentTypes []string
}
func (c *config) validate() error {
if c.level != gzip.DefaultCompression && (c.level < gzip.BestSpeed || c.level > gzip.BestCompression) {
return fmt.Errorf("invalid compression level requested: %d", c.level)
}
if c.minSize < 0 {
return fmt.Errorf("minimum size must be more than zero")
}
return nil
}
type option func(c *config)
func MinSize(size int) option {
return func(c *config) {
c.minSize = size
}
}
func CompressionLevel(level int) option {
return func(c *config) {
c.level = level
}
}
func ContentTypes(types []string) option {
return func(c *config) {
c.contentTypes = []string{}
for _, v := range types {
c.contentTypes = append(c.contentTypes, strings.ToLower(v))
}
}
}
// GzipHandler wraps an HTTP handler, to transparently gzip the response body if // GzipHandler wraps an HTTP handler, to transparently gzip the response body if
// the client supports it (via the Accept-Encoding header). This will compress at // the client supports it (via the Accept-Encoding header). This will compress at
// the default compression level. // the default compression level.
@ -282,6 +334,23 @@ func acceptsGzip(r *http.Request) bool {
return acceptedEncodings["gzip"] > 0.0 return acceptedEncodings["gzip"] > 0.0
} }
// returns true if we've been configured to compress the specific content type.
func handleContentType(contentTypes []string, w http.ResponseWriter) bool {
// If contentTypes is empty we handle all content types.
if len(contentTypes) == 0 {
return true
}
ct := strings.ToLower(w.Header().Get(contentType))
for _, c := range contentTypes {
if c == ct {
return true
}
}
return false
}
// parseEncodings attempts to parse a list of codings, per RFC 2616, as might // parseEncodings attempts to parse a list of codings, per RFC 2616, as might
// appear in an Accept-Encoding header. It returns a map of content-codings to // appear in an Accept-Encoding header. It returns a map of content-codings to
// quality values, and an error containing the errors encountered. It's probably // quality values, and an error containing the errors encountered. It's probably

View file

@ -23,6 +23,7 @@ type GzipWriter interface {
SetResponseWriter(http.ResponseWriter) SetResponseWriter(http.ResponseWriter)
setIndex(int) setIndex(int)
setMinSize(int) setMinSize(int)
setContentTypes([]string)
} }
func (w *GzipResponseWriter) SetResponseWriter(rw http.ResponseWriter) { func (w *GzipResponseWriter) SetResponseWriter(rw http.ResponseWriter) {
@ -37,6 +38,10 @@ func (w *GzipResponseWriter) setMinSize(minSize int) {
w.minSize = minSize w.minSize = minSize
} }
func (w *GzipResponseWriter) setContentTypes(contentTypes []string) {
w.contentTypes = contentTypes
}
// -------- // --------
type GzipResponseWriterWrapper struct { type GzipResponseWriterWrapper struct {
@ -45,6 +50,9 @@ type GzipResponseWriterWrapper struct {
func (g *GzipResponseWriterWrapper) Write(b []byte) (int, error) { func (g *GzipResponseWriterWrapper) Write(b []byte) (int, error) {
if g.gw == nil && isEncoded(g.Header()) { if g.gw == nil && isEncoded(g.Header()) {
if g.code != 0 {
g.ResponseWriter.WriteHeader(g.code)
}
return g.ResponseWriter.Write(b) return g.ResponseWriter.Write(b)
} }
return g.GzipResponseWriter.Write(b) return g.GzipResponseWriter.Write(b)