-
Notifications
You must be signed in to change notification settings - Fork 617
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Issue #119: Transparent response body compression
This patch adds an option 'proxy.gzip.contentype' which enables transparent response body compression if the client requests it, the content type of the response matches the proxy.gzip.contenttype regexp and the response is not already compressed. The gzip handler is mostly based on the code from @smanke from https://github.com/smanke/handler/gzip.
- Loading branch information
1 parent
264345f
commit 3c71d66
Showing
8 changed files
with
304 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
// Package assert provides a simple assert framework. | ||
package assert | ||
|
||
import ( | ||
"fmt" | ||
"path/filepath" | ||
"reflect" | ||
"runtime" | ||
"testing" | ||
) | ||
|
||
// Equal provides an assertEqual function | ||
func Equal(t *testing.T) func(got, want interface{}) { | ||
return EqualDepth(t, 1, "") | ||
} | ||
|
||
func EqualDepth(t *testing.T, calldepth int, desc string) func(got, want interface{}) { | ||
return func(got, want interface{}) { | ||
_, file, line, _ := runtime.Caller(calldepth) | ||
if !reflect.DeepEqual(got, want) { | ||
fmt.Printf("\t%s:%d: %s: got %v want %v\n", filepath.Base(file), line, desc, got, want) | ||
t.Fail() | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
// Copyright (c) 2016 Sebastian Mancke and eBay, both MIT licensed | ||
|
||
// Package gzip provides an HTTP handler which compresses responses | ||
// if the client supports this, the response is compressable and | ||
// not already compressed. | ||
// | ||
// Based on https://github.com/smancke/handler/gzip | ||
package gzip | ||
|
||
import ( | ||
"compress/gzip" | ||
"io" | ||
"net/http" | ||
"regexp" | ||
"strings" | ||
"sync" | ||
) | ||
|
||
const ( | ||
headerVary = "Vary" | ||
headerAcceptEncoding = "Accept-Encoding" | ||
headerContentEncoding = "Content-Encoding" | ||
headerContentType = "Content-Type" | ||
headerContentLength = "Content-Length" | ||
encodingGzip = "gzip" | ||
) | ||
|
||
var gzipWriterPool = sync.Pool{ | ||
New: func() interface{} { return gzip.NewWriter(nil) }, | ||
} | ||
|
||
// NewGzipHandler wraps an existing handler to transparently gzip the response | ||
// body if the client supports it (via the Accept-Encoding header) and the | ||
// response Content-Type matches the contentTypes expression. | ||
func NewGzipHandler(h http.Handler, contentTypes *regexp.Regexp) http.Handler { | ||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
w.Header().Add(headerVary, headerAcceptEncoding) | ||
|
||
if acceptsGzip(r) { | ||
gzWriter := NewGzipResponseWriter(w, contentTypes) | ||
defer gzWriter.Close() | ||
h.ServeHTTP(gzWriter, r) | ||
} else { | ||
h.ServeHTTP(w, r) | ||
} | ||
}) | ||
} | ||
|
||
type GzipResponseWriter struct { | ||
writer io.Writer | ||
gzipWriter *gzip.Writer | ||
contentTypes *regexp.Regexp | ||
http.ResponseWriter | ||
} | ||
|
||
func NewGzipResponseWriter(w http.ResponseWriter, contentTypes *regexp.Regexp) *GzipResponseWriter { | ||
return &GzipResponseWriter{ResponseWriter: w, contentTypes: contentTypes} | ||
} | ||
|
||
func (grw *GzipResponseWriter) WriteHeader(code int) { | ||
if grw.writer == nil { | ||
if isCompressable(grw.Header(), grw.contentTypes) { | ||
grw.Header().Del(headerContentLength) | ||
grw.Header().Set(headerContentEncoding, encodingGzip) | ||
grw.gzipWriter = gzipWriterPool.Get().(*gzip.Writer) | ||
grw.gzipWriter.Reset(grw.ResponseWriter) | ||
|
||
grw.writer = grw.gzipWriter | ||
} else { | ||
grw.writer = grw.ResponseWriter | ||
} | ||
} | ||
grw.ResponseWriter.WriteHeader(code) | ||
} | ||
|
||
func (grw *GzipResponseWriter) Write(b []byte) (int, error) { | ||
if grw.writer == nil { | ||
if _, ok := grw.Header()[headerContentType]; !ok { | ||
// Set content-type if not present. Otherwise golang would make application/gzip out of that. | ||
grw.Header().Set(headerContentType, http.DetectContentType(b)) | ||
} | ||
grw.WriteHeader(http.StatusOK) | ||
} | ||
return grw.writer.Write(b) | ||
} | ||
|
||
func (grw *GzipResponseWriter) Close() { | ||
if grw.gzipWriter != nil { | ||
grw.gzipWriter.Close() | ||
gzipWriterPool.Put(grw.gzipWriter) | ||
} | ||
} | ||
|
||
func isCompressable(header http.Header, contentTypes *regexp.Regexp) bool { | ||
// don't compress if it is already encoded | ||
if header.Get(headerContentEncoding) != "" { | ||
return false | ||
} | ||
return contentTypes.MatchString(header.Get(headerContentType)) | ||
} | ||
|
||
func acceptsGzip(r *http.Request) bool { | ||
return strings.Contains(r.Header.Get(headerAcceptEncoding), encodingGzip) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
// Copyright (c) 2016 Sebastian Mancke and eBay, both MIT licensed | ||
package gzip | ||
|
||
import ( | ||
"bytes" | ||
"compress/gzip" | ||
"io/ioutil" | ||
"net/http" | ||
"net/http/httptest" | ||
"regexp" | ||
"strconv" | ||
"testing" | ||
|
||
"github.com/eBay/fabio/assert" | ||
) | ||
|
||
var contentTypes = regexp.MustCompile(`^(text/.*|application/(javascript|json|font-woff|xml)|.*\+(json|xml))$`) | ||
|
||
func Test_GzipHandler_CompressableType(t *testing.T) { | ||
server := httptest.NewServer(NewGzipHandler(test_text_handler(), contentTypes)) | ||
|
||
assertEqual := assert.Equal(t) | ||
|
||
r, err := http.NewRequest("GET", server.URL, nil) | ||
assertEqual(err, nil) | ||
r.Header.Set("Accept-Encoding", "gzip") | ||
|
||
resp, err := http.DefaultClient.Do(r) | ||
assertEqual(err, nil) | ||
|
||
assertEqual(resp.Header.Get("Content-Type"), "text/plain; charset=utf-8") | ||
assertEqual(resp.Header.Get("Content-Encoding"), "gzip") | ||
|
||
gzBytes, err := ioutil.ReadAll(resp.Body) | ||
assertEqual(err, nil) | ||
assertEqual(resp.Header.Get("Content-Length"), strconv.Itoa(len(gzBytes))) | ||
|
||
reader, err := gzip.NewReader(bytes.NewBuffer(gzBytes)) | ||
assertEqual(err, nil) | ||
defer reader.Close() | ||
|
||
bytes, err := ioutil.ReadAll(reader) | ||
assertEqual(err, nil) | ||
|
||
assertEqual(string(bytes), "Hello World") | ||
} | ||
|
||
func Test_GzipHandler_NotCompressingTwice(t *testing.T) { | ||
server := httptest.NewServer(NewGzipHandler(test_already_compressed_handler(), contentTypes)) | ||
|
||
assertEqual := assert.Equal(t) | ||
|
||
r, err := http.NewRequest("GET", server.URL, nil) | ||
assertEqual(err, nil) | ||
r.Header.Set("Accept-Encoding", "gzip") | ||
|
||
resp, err := http.DefaultClient.Do(r) | ||
assertEqual(err, nil) | ||
|
||
assertEqual(resp.Header.Get("Content-Encoding"), "gzip") | ||
|
||
reader, err := gzip.NewReader(resp.Body) | ||
assertEqual(err, nil) | ||
defer reader.Close() | ||
|
||
bytes, err := ioutil.ReadAll(reader) | ||
assertEqual(err, nil) | ||
|
||
assertEqual(string(bytes), "Hello World") | ||
} | ||
|
||
func Test_GzipHandler_CompressableType_NoAccept(t *testing.T) { | ||
server := httptest.NewServer(NewGzipHandler(test_text_handler(), contentTypes)) | ||
|
||
assertEqual := assert.Equal(t) | ||
|
||
r, err := http.NewRequest("GET", server.URL, nil) | ||
assertEqual(err, nil) | ||
r.Header.Set("Accept-Encoding", "none") | ||
|
||
resp, err := http.DefaultClient.Do(r) | ||
assertEqual(err, nil) | ||
|
||
assertEqual(resp.Header.Get("Content-Encoding"), "") | ||
|
||
bytes, err := ioutil.ReadAll(resp.Body) | ||
assertEqual(err, nil) | ||
|
||
assertEqual(string(bytes), "Hello World") | ||
} | ||
|
||
func Test_GzipHandler_NonCompressableType(t *testing.T) { | ||
server := httptest.NewServer(NewGzipHandler(test_binary_handler(), contentTypes)) | ||
|
||
assertEqual := assert.Equal(t) | ||
|
||
r, err := http.NewRequest("GET", server.URL, nil) | ||
assertEqual(err, nil) | ||
r.Header.Set("Accept-Encoding", "gzip") | ||
|
||
resp, err := http.DefaultClient.Do(r) | ||
assertEqual(err, nil) | ||
|
||
assertEqual(resp.Header.Get("Content-Encoding"), "") | ||
|
||
bytes, err := ioutil.ReadAll(resp.Body) | ||
assertEqual(err, nil) | ||
|
||
assertEqual(bytes, []byte{42}) | ||
} | ||
|
||
func test_text_handler() http.Handler { | ||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
b := []byte("Hello World") | ||
w.Header().Set("Content-Length", strconv.Itoa(len(b))) | ||
w.Write(b) | ||
}) | ||
} | ||
|
||
func test_binary_handler() http.Handler { | ||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
w.Header().Set("Content-Type", "image/jpg") | ||
w.Write([]byte{42}) | ||
}) | ||
} | ||
|
||
func test_already_compressed_handler() http.Handler { | ||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
w.Header().Set("Content-Encoding", "gzip") | ||
gzWriter := gzip.NewWriter(w) | ||
gzWriter.Write([]byte("Hello World")) | ||
gzWriter.Close() | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters