Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 65 additions & 9 deletions gzip.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"compress/gzip"
"fmt"
"io"
"mime"
"net"
"net/http"
"strconv"
Expand All @@ -28,8 +29,8 @@ const (
// The examples seem to indicate that it is.
DefaultQValue = 1.0

// 1500 bytes is the MTU size for the internet since that is the largest size allowed at the network layer.
// If you take a file that is 1300 bytes and compress it to 800 bytes, it’s still transmitted in that same 1500 byte packet regardless, so you’ve gained nothing.
// 1500 bytes is the MTU size for the internet since that is the largest size allowed at the network layer.
// If you take a file that is 1300 bytes and compress it to 800 bytes, it’s still transmitted in that same 1500 byte packet regardless, so you’ve gained nothing.
// That being the case, you should restrict the gzip compression to files with a size greater than a single packet, 1400 bytes (1.4KB) is a safe value.
DefaultMinSize = 1400
)
Expand Down Expand Up @@ -82,7 +83,7 @@ type GzipResponseWriter struct {
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.

contentTypes []string // Only compress if the response is one of these content-types. All are accepted if empty.
contentTypes []parsedContentType // Only compress if the response is one of these content-types. All are accepted if empty.
}

type GzipResponseWriterWithCloseNotify struct {
Expand Down Expand Up @@ -296,11 +297,40 @@ func GzipHandlerWithOpts(opts ...option) (func(http.Handler) http.Handler, error
}, nil
}

// Parsed representation of one of the inputs to ContentTypes.
// See https://golang.org/pkg/mime/#ParseMediaType
type parsedContentType struct {
mediaType string
params map[string]string
}

// equals returns whether this content type matches another content type.
func (pct parsedContentType) equals(mediaType string, params map[string]string) bool {
if pct.mediaType != mediaType {
return false
}
// if pct has no params, don't care about other's params
if len(pct.params) == 0 {
return true
}

// if pct has any params, they must be identical to other's.
if len(pct.params) != len(params) {
return false
}
for k, v := range pct.params {
if w, ok := params[k]; !ok || v != w {
return false
}
}
return true
}

// Used for functional configuration.
type config struct {
minSize int
level int
contentTypes []string
contentTypes []parsedContentType
}

func (c *config) validate() error {
Expand Down Expand Up @@ -329,11 +359,32 @@ func CompressionLevel(level int) option {
}
}

// ContentTypes specifies a list of content types to compare
// the Content-Type header to before compressing. If none
// match, the response will be returned as-is.
//
// Content types are compared in a case-insensitive, whitespace-ignored
// manner.
//
// A MIME type without any other directive will match a content type
// that has the same MIME type, regardless of that content type's other
// directives. I.e., "text/html" will match both "text/html" and
// "text/html; charset=utf-8".
//
// A MIME type with any other directive will only match a content type
// that has the same MIME type and other directives. I.e.,
// "text/html; charset=utf-8" will only match "text/html; charset=utf-8".
//
// By default, responses are gzipped regardless of
// Content-Type.
func ContentTypes(types []string) option {
return func(c *config) {
c.contentTypes = []string{}
c.contentTypes = []parsedContentType{}
for _, v := range types {
c.contentTypes = append(c.contentTypes, strings.ToLower(v))
mediaType, params, err := mime.ParseMediaType(v)
if err == nil {
c.contentTypes = append(c.contentTypes, parsedContentType{mediaType, params})
}
}
}
}
Expand All @@ -354,15 +405,20 @@ func acceptsGzip(r *http.Request) bool {
}

// returns true if we've been configured to compress the specific content type.
func handleContentType(contentTypes []string, w http.ResponseWriter) bool {
func handleContentType(contentTypes []parsedContentType, 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))
ct := w.Header().Get(contentType)
mediaType, params, err := mime.ParseMediaType(ct)
if err != nil {
return false
}

for _, c := range contentTypes {
if c == ct {
if c.equals(mediaType, params) {
return true
}
}
Expand Down
50 changes: 39 additions & 11 deletions gzip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ func TestFlushBeforeWrite(t *testing.T) {
func TestImplementCloseNotifier(t *testing.T) {
request := httptest.NewRequest(http.MethodGet, "/", nil)
request.Header.Set(acceptEncoding, "gzip")
GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request){
GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
_, ok := rw.(http.CloseNotifier)
assert.True(t, ok, "response writer must implement http.CloseNotifier")
})).ServeHTTP(&mockRWCloseNotify{}, request)
Expand All @@ -354,7 +354,7 @@ func TestImplementCloseNotifier(t *testing.T) {
func TestImplementFlusherAndCloseNotifier(t *testing.T) {
request := httptest.NewRequest(http.MethodGet, "/", nil)
request.Header.Set(acceptEncoding, "gzip")
GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request){
GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
_, okCloseNotifier := rw.(http.CloseNotifier)
assert.True(t, okCloseNotifier, "response writer must implement http.CloseNotifier")
_, okFlusher := rw.(http.Flusher)
Expand All @@ -365,13 +365,12 @@ func TestImplementFlusherAndCloseNotifier(t *testing.T) {
func TestNotImplementCloseNotifier(t *testing.T) {
request := httptest.NewRequest(http.MethodGet, "/", nil)
request.Header.Set(acceptEncoding, "gzip")
GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request){
GzipHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
_, ok := rw.(http.CloseNotifier)
assert.False(t, ok, "response writer must not implement http.CloseNotifier")
})).ServeHTTP(httptest.NewRecorder(), request)
}


type mockRWCloseNotify struct{}

func (m *mockRWCloseNotify) CloseNotify() <-chan bool {
Expand All @@ -390,7 +389,6 @@ func (m *mockRWCloseNotify) WriteHeader(int) {
panic("implement me")
}


func TestIgnoreSubsequentWriteHeader(t *testing.T) {
handler := GzipHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(500)
Expand Down Expand Up @@ -444,23 +442,53 @@ var contentTypeTests = []struct {
expectedGzip: true,
},
{
name: "Exact content-type match",
name: "MIME match",
contentType: "application/json",
acceptedContentTypes: []string{"application/json"},
expectedGzip: true,
},
{
name: "Case insensitive content-type matching",
contentType: "Application/Json",
name: "MIME no match",
contentType: "text/xml",
acceptedContentTypes: []string{"application/json"},
expectedGzip: true,
expectedGzip: false,
},
{
name: "Non-matching content-type",
contentType: "text/xml",
name: "MIME match with no other directive ignores non-MIME directives",
contentType: "application/json; charset=utf-8",
acceptedContentTypes: []string{"application/json"},
expectedGzip: true,
},
{
name: "MIME match with other directives requires all directives be equal, different charset",
contentType: "application/json; charset=ascii",
acceptedContentTypes: []string{"application/json; charset=utf-8"},
expectedGzip: false,
},
{
name: "MIME match with other directives requires all directives be equal, same charset",
contentType: "application/json; charset=utf-8",
acceptedContentTypes: []string{"application/json; charset=utf-8"},
expectedGzip: true,
},
{
name: "MIME match with other directives requires all directives be equal, missing charset",
contentType: "application/json",
acceptedContentTypes: []string{"application/json; charset=ascii"},
expectedGzip: false,
},
{
name: "MIME match case insensitive",
contentType: "Application/Json",
acceptedContentTypes: []string{"application/json"},
expectedGzip: true,
},
{
name: "MIME match ignore whitespace",
contentType: "application/json;charset=utf-8",
acceptedContentTypes: []string{"application/json; charset=utf-8"},
expectedGzip: true,
},
}

func TestContentTypes(t *testing.T) {
Expand Down