2017-02-08 00:33:23 +03:00
package gziphandler
import (
"bufio"
"compress/gzip"
2017-07-13 18:56:01 +03:00
"errors"
2017-02-08 00:33:23 +03:00
"fmt"
2017-04-11 18:10:46 +03:00
"io"
2017-02-08 00:33:23 +03:00
"net"
"net/http"
"strconv"
"strings"
"sync"
)
const (
vary = "Vary"
acceptEncoding = "Accept-Encoding"
contentEncoding = "Content-Encoding"
contentType = "Content-Type"
contentLength = "Content-Length"
)
type codings map [ string ] float64
2017-04-11 18:10:46 +03:00
const (
// DefaultQValue is the default qvalue to assign to an encoding if no explicit qvalue is set.
// This is actually kind of ambiguous in RFC 2616, so hopefully it's correct.
// The examples seem to indicate that it is.
DefaultQValue = 1.0
// DefaultMinSize defines the minimum size to reach to enable compression.
// It's 512 bytes.
DefaultMinSize = 512
)
2017-02-08 00:33:23 +03:00
// gzipWriterPools stores a sync.Pool for each compression level for reuse of
// gzip.Writers. Use poolIndex to covert a compression level to an index into
// gzipWriterPools.
var gzipWriterPools [ gzip . BestCompression - gzip . BestSpeed + 2 ] * sync . Pool
func init ( ) {
for i := gzip . BestSpeed ; i <= gzip . BestCompression ; i ++ {
addLevelPool ( i )
}
addLevelPool ( gzip . DefaultCompression )
}
// poolIndex maps a compression level to its index into gzipWriterPools. It
// assumes that level is a valid gzip compression level.
func poolIndex ( level int ) int {
// gzip.DefaultCompression == -1, so we need to treat it special.
if level == gzip . DefaultCompression {
return gzip . BestCompression - gzip . BestSpeed + 1
}
return level - gzip . BestSpeed
}
func addLevelPool ( level int ) {
gzipWriterPools [ poolIndex ( level ) ] = & sync . Pool {
New : func ( ) interface { } {
// NewWriterLevel only returns error on a bad level, we are guaranteeing
// that this will be a valid level so it is okay to ignore the returned
// error.
w , _ := gzip . NewWriterLevel ( nil , level )
return w
} ,
}
}
// GzipResponseWriter provides an http.ResponseWriter interface, which gzips
// bytes before writing them to the underlying response. This doesn't close the
// writers, so don't forget to do that.
2017-04-11 18:10:46 +03:00
// It can be configured to skip response smaller than minSize.
2017-02-08 00:33:23 +03:00
type GzipResponseWriter struct {
http . ResponseWriter
index int // Index for gzipWriterPools.
gw * gzip . Writer
2017-04-11 18:10:46 +03:00
code int // Saves the WriteHeader value.
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.
2017-08-21 12:10:03 +03:00
contentTypes [ ] string // Only compress if the response is one of these content-types. All are accepted if empty.
2017-02-08 00:33:23 +03:00
}
// Write appends data to the gzip writer.
func ( w * GzipResponseWriter ) Write ( b [ ] byte ) ( int , error ) {
2017-04-11 18:10:46 +03:00
// If content type is not set.
2017-02-08 00:33:23 +03:00
if _ , ok := w . Header ( ) [ contentType ] ; ! ok {
2017-04-11 18:10:46 +03:00
// It infer it from the uncompressed body.
2017-02-08 00:33:23 +03:00
w . Header ( ) . Set ( contentType , http . DetectContentType ( b ) )
}
2017-04-11 18:10:46 +03:00
// GZIP responseWriter is initialized. Use the GZIP responseWriter.
if w . gw != nil {
n , err := w . gw . Write ( b )
return n , err
}
// Save the write into a buffer for later use in GZIP responseWriter (if content is long enough) or at close with regular responseWriter.
2017-07-13 18:56:01 +03:00
// On the first write, w.buf changes from nil to a valid slice
2017-04-11 18:10:46 +03:00
w . buf = append ( w . buf , b ... )
2017-08-21 12:10:03 +03:00
// If the global writes are bigger than the minSize and we're about to write
// a response containing a content type we want to handle, enable
// compression.
if len ( w . buf ) >= w . minSize && handleContentType ( w . contentTypes , w ) {
2017-04-11 18:10:46 +03:00
err := w . startGzip ( )
if err != nil {
return 0 , err
}
}
return len ( b ) , nil
}
// startGzip initialize any GZIP specific informations.
func ( w * GzipResponseWriter ) startGzip ( ) error {
// Set the GZIP header.
w . Header ( ) . Set ( contentEncoding , "gzip" )
// if the Content-Length is already set, then calls to Write on gzip
// will fail to set the Content-Length header since its already set
// See: https://github.com/golang/go/issues/14975.
w . Header ( ) . Del ( contentLength )
// Write the header to gzip response.
2017-07-13 18:56:01 +03:00
if w . code != 0 {
w . ResponseWriter . WriteHeader ( w . code )
}
2017-04-11 18:10:46 +03:00
// Initialize the GZIP response.
w . init ( )
// Flush the buffer into the gzip reponse.
n , err := w . gw . Write ( w . buf )
// This should never happen (per io.Writer docs), but if the write didn't
// accept the entire buffer but returned no specific error, we have no clue
// what's going on, so abort just to be safe.
if err == nil && n < len ( w . buf ) {
return io . ErrShortWrite
}
w . buf = nil
return err
2017-02-08 00:33:23 +03:00
}
2017-04-11 18:10:46 +03:00
// WriteHeader just saves the response code until close or GZIP effective writes.
2017-02-08 00:33:23 +03:00
func ( w * GzipResponseWriter ) WriteHeader ( code int ) {
2017-04-11 18:10:46 +03:00
w . code = code
}
2017-02-08 00:33:23 +03:00
// init graps a new gzip writer from the gzipWriterPool and writes the correct
// content encoding header.
func ( w * GzipResponseWriter ) init ( ) {
// Bytes written during ServeHTTP are redirected to this gzip writer
// before being written to the underlying response.
gzw := gzipWriterPools [ w . index ] . Get ( ) . ( * gzip . Writer )
gzw . Reset ( w . ResponseWriter )
w . gw = gzw
}
// Close will close the gzip.Writer and will put it back in the gzipWriterPool.
func ( w * GzipResponseWriter ) Close ( ) error {
if w . gw == nil {
2017-07-13 18:56:01 +03:00
// Gzip not trigged yet, write out regular response.
if w . code != 0 {
w . ResponseWriter . WriteHeader ( w . code )
}
if w . buf != nil {
_ , writeErr := w . ResponseWriter . Write ( w . buf )
// Returns the error if any at write.
if writeErr != nil {
return fmt . Errorf ( "gziphandler: write to regular responseWriter at close gets error: %q" , writeErr . Error ( ) )
}
}
2017-02-08 00:33:23 +03:00
return nil
}
err := w . gw . Close ( )
gzipWriterPools [ w . index ] . Put ( w . gw )
2017-04-11 18:10:46 +03:00
w . gw = nil
2017-02-08 00:33:23 +03:00
return err
}
// Flush flushes the underlying *gzip.Writer and then the underlying
// http.ResponseWriter if it is an http.Flusher. This makes GzipResponseWriter
// an http.Flusher.
func ( w * GzipResponseWriter ) Flush ( ) {
if w . gw != nil {
w . gw . Flush ( )
}
if fw , ok := w . ResponseWriter . ( http . Flusher ) ; ok {
fw . Flush ( )
}
}
// Hijack implements http.Hijacker. If the underlying ResponseWriter is a
// Hijacker, its Hijack method is returned. Otherwise an error is returned.
func ( w * GzipResponseWriter ) Hijack ( ) ( net . Conn , * bufio . ReadWriter , error ) {
if hj , ok := w . ResponseWriter . ( http . Hijacker ) ; ok {
return hj . Hijack ( )
}
return nil , nil , fmt . Errorf ( "http.Hijacker interface is not supported" )
}
// verify Hijacker interface implementation
var _ http . Hijacker = & GzipResponseWriter { }
// MustNewGzipLevelHandler behaves just like NewGzipLevelHandler except that in
// an error case it panics rather than returning an error.
func MustNewGzipLevelHandler ( level int ) func ( http . Handler ) http . Handler {
wrap , err := NewGzipLevelHandler ( level )
if err != nil {
panic ( err )
}
return wrap
}
// NewGzipLevelHandler returns a wrapper function (often known as middleware)
// which can be used to wrap an HTTP handler to transparently gzip the response
// body if the client supports it (via the Accept-Encoding header). Responses will
// be encoded at the given gzip compression level. An error will be returned only
// if an invalid gzip compression level is given, so if one can ensure the level
// is valid, the returned error can be safely ignored.
func NewGzipLevelHandler ( level int ) ( func ( http . Handler ) http . Handler , error ) {
2017-04-11 18:10:46 +03:00
return NewGzipLevelAndMinSize ( level , DefaultMinSize )
}
// NewGzipLevelAndMinSize behave as NewGzipLevelHandler except it let the caller
// specify the minimum size before compression.
func NewGzipLevelAndMinSize ( level , minSize int ) ( func ( http . Handler ) http . Handler , error ) {
2017-08-21 12:10:03 +03:00
return GzipHandlerWithOpts ( & GzipResponseWriter { } , CompressionLevel ( level ) , MinSize ( minSize ) )
2017-07-13 18:56:01 +03:00
}
2017-08-21 12:10:03 +03:00
func GzipHandlerWithOpts ( gw GzipWriter , opts ... option ) ( func ( http . Handler ) http . Handler , error ) {
2017-07-13 18:56:01 +03:00
if gw == nil {
return nil , errors . New ( "the GzipWriter must be defined" )
}
2017-08-21 12:10:03 +03:00
c := & config {
level : gzip . DefaultCompression ,
minSize : DefaultMinSize ,
}
for _ , o := range opts {
o ( c )
}
if err := c . validate ( ) ; err != nil {
return nil , err
}
2017-02-08 00:33:23 +03:00
return func ( h http . Handler ) http . Handler {
2017-08-21 12:10:03 +03:00
index := poolIndex ( c . level )
2017-02-08 00:33:23 +03:00
return http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
w . Header ( ) . Add ( vary , acceptEncoding )
if acceptsGzip ( r ) {
2017-07-13 18:56:01 +03:00
gw . SetResponseWriter ( w )
gw . setIndex ( index )
2017-08-21 12:10:03 +03:00
gw . setMinSize ( c . minSize )
gw . setContentTypes ( c . contentTypes )
2017-02-08 00:33:23 +03:00
defer gw . Close ( )
h . ServeHTTP ( gw , r )
} else {
h . ServeHTTP ( w , r )
}
} )
} , nil
}
2017-08-21 12:10:03 +03:00
// 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 ) )
}
}
}
2017-02-08 00:33:23 +03:00
// 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 default compression level.
func GzipHandler ( h http . Handler ) http . Handler {
wrapper , _ := NewGzipLevelHandler ( gzip . DefaultCompression )
return wrapper ( h )
}
// acceptsGzip returns true if the given HTTP request indicates that it will
// accept a gzipped response.
func acceptsGzip ( r * http . Request ) bool {
acceptedEncodings , _ := parseEncodings ( r . Header . Get ( acceptEncoding ) )
return acceptedEncodings [ "gzip" ] > 0.0
}
2017-08-21 12:10:03 +03:00
// 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
}
2017-02-08 00:33:23 +03:00
// 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
// quality values, and an error containing the errors encountered. It's probably
// safe to ignore those, because silently ignoring errors is how the internet
// works.
//
// See: http://tools.ietf.org/html/rfc2616#section-14.3.
func parseEncodings ( s string ) ( codings , error ) {
c := make ( codings )
var e [ ] string
for _ , ss := range strings . Split ( s , "," ) {
coding , qvalue , err := parseCoding ( ss )
if err != nil {
e = append ( e , err . Error ( ) )
} else {
c [ coding ] = qvalue
}
}
// TODO (adammck): Use a proper multi-error struct, so the individual errors
// can be extracted if anyone cares.
if len ( e ) > 0 {
return c , fmt . Errorf ( "errors while parsing encodings: %s" , strings . Join ( e , ", " ) )
}
return c , nil
}
// parseCoding parses a single conding (content-coding with an optional qvalue),
// as might appear in an Accept-Encoding header. It attempts to forgive minor
// formatting errors.
func parseCoding ( s string ) ( coding string , qvalue float64 , err error ) {
for n , part := range strings . Split ( s , ";" ) {
part = strings . TrimSpace ( part )
2017-04-11 18:10:46 +03:00
qvalue = DefaultQValue
2017-02-08 00:33:23 +03:00
if n == 0 {
coding = strings . ToLower ( part )
} else if strings . HasPrefix ( part , "q=" ) {
qvalue , err = strconv . ParseFloat ( strings . TrimPrefix ( part , "q=" ) , 64 )
if qvalue < 0.0 {
qvalue = 0.0
} else if qvalue > 1.0 {
qvalue = 1.0
}
}
}
if coding == "" {
err = fmt . Errorf ( "empty content-coding" )
}
return
}