-
Notifications
You must be signed in to change notification settings - Fork 619
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 691401e
Showing
10 changed files
with
454 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,35 @@ | ||
package gzip | ||
|
||
import "testing" | ||
|
||
// TestContentTypes tests the content-type regexp that is used as | ||
// an example in fabio.properties | ||
func TestContentTypes(t *testing.T) { | ||
tests := []string{ | ||
"text/foo", | ||
"text/foo; charset=UTF-8", | ||
"text/plain", | ||
"text/plain; charset=UTF-8", | ||
"application/json", | ||
"application/json; charset=UTF-8", | ||
"application/javascript", | ||
"application/javascript; charset=UTF-8", | ||
"application/font-woff", | ||
"application/font-woff; charset=UTF-8", | ||
"application/xml", | ||
"application/xml; charset=UTF-8", | ||
"vendor/vendor.foo+json", | ||
"vendor/vendor.foo+json; charset=UTF-8", | ||
"vendor/vendor.foo+xml", | ||
"vendor/vendor.foo+xml; charset=UTF-8", | ||
} | ||
|
||
for _, tt := range tests { | ||
tt := tt // capture loop var | ||
t.Run(tt, func(t *testing.T) { | ||
if !contentTypes.MatchString(tt) { | ||
t.Fatalf("%q does not match content types regexp", tt) | ||
} | ||
}) | ||
} | ||
} |
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) | ||
} |
Oops, something went wrong.