1package gziphandler 2 3import ( 4 "bufio" 5 "compress/gzip" 6 "fmt" 7 "io" 8 "net" 9 "net/http" 10 "strconv" 11 "strings" 12 "sync" 13) 14 15const ( 16 vary = "Vary" 17 acceptEncoding = "Accept-Encoding" 18 contentEncoding = "Content-Encoding" 19 contentType = "Content-Type" 20 contentLength = "Content-Length" 21) 22 23type codings map[string]float64 24 25const ( 26 // DefaultQValue is the default qvalue to assign to an encoding if no explicit qvalue is set. 27 // This is actually kind of ambiguous in RFC 2616, so hopefully it's correct. 28 // The examples seem to indicate that it is. 29 DefaultQValue = 1.0 30 31 // 1500 bytes is the MTU size for the internet since that is the largest size allowed at the network layer. 32 // 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. 33 // 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. 34 DefaultMinSize = 1400 35) 36 37// gzipWriterPools stores a sync.Pool for each compression level for reuse of 38// gzip.Writers. Use poolIndex to covert a compression level to an index into 39// gzipWriterPools. 40var gzipWriterPools [gzip.BestCompression - gzip.BestSpeed + 2]*sync.Pool 41 42func init() { 43 for i := gzip.BestSpeed; i <= gzip.BestCompression; i++ { 44 addLevelPool(i) 45 } 46 addLevelPool(gzip.DefaultCompression) 47} 48 49// poolIndex maps a compression level to its index into gzipWriterPools. It 50// assumes that level is a valid gzip compression level. 51func poolIndex(level int) int { 52 // gzip.DefaultCompression == -1, so we need to treat it special. 53 if level == gzip.DefaultCompression { 54 return gzip.BestCompression - gzip.BestSpeed + 1 55 } 56 return level - gzip.BestSpeed 57} 58 59func addLevelPool(level int) { 60 gzipWriterPools[poolIndex(level)] = &sync.Pool{ 61 New: func() interface{} { 62 // NewWriterLevel only returns error on a bad level, we are guaranteeing 63 // that this will be a valid level so it is okay to ignore the returned 64 // error. 65 w, _ := gzip.NewWriterLevel(nil, level) 66 return w 67 }, 68 } 69} 70 71// GzipResponseWriter provides an http.ResponseWriter interface, which gzips 72// bytes before writing them to the underlying response. This doesn't close the 73// writers, so don't forget to do that. 74// It can be configured to skip response smaller than minSize. 75type GzipResponseWriter struct { 76 http.ResponseWriter 77 index int // Index for gzipWriterPools. 78 gw *gzip.Writer 79 80 code int // Saves the WriteHeader value. 81 82 minSize int // Specifed the minimum response size to gzip. If the response length is bigger than this value, it is compressed. 83 buf []byte // Holds the first part of the write before reaching the minSize or the end of the write. 84 85 contentTypes []string // Only compress if the response is one of these content-types. All are accepted if empty. 86} 87 88type GzipResponseWriterWithCloseNotify struct { 89 *GzipResponseWriter 90} 91 92func (w GzipResponseWriterWithCloseNotify) CloseNotify() <-chan bool { 93 return w.ResponseWriter.(http.CloseNotifier).CloseNotify() 94} 95 96// Write appends data to the gzip writer. 97func (w *GzipResponseWriter) Write(b []byte) (int, error) { 98 // If content type is not set. 99 if _, ok := w.Header()[contentType]; !ok { 100 // It infer it from the uncompressed body. 101 w.Header().Set(contentType, http.DetectContentType(b)) 102 } 103 104 // GZIP responseWriter is initialized. Use the GZIP responseWriter. 105 if w.gw != nil { 106 n, err := w.gw.Write(b) 107 return n, err 108 } 109 110 // Save the write into a buffer for later use in GZIP responseWriter (if content is long enough) or at close with regular responseWriter. 111 // On the first write, w.buf changes from nil to a valid slice 112 w.buf = append(w.buf, b...) 113 114 // If the global writes are bigger than the minSize and we're about to write 115 // a response containing a content type we want to handle, enable 116 // compression. 117 if len(w.buf) >= w.minSize && handleContentType(w.contentTypes, w) && w.Header().Get(contentEncoding) == "" { 118 err := w.startGzip() 119 if err != nil { 120 return 0, err 121 } 122 } 123 124 return len(b), nil 125} 126 127// startGzip initialize any GZIP specific informations. 128func (w *GzipResponseWriter) startGzip() error { 129 130 // Set the GZIP header. 131 w.Header().Set(contentEncoding, "gzip") 132 133 // if the Content-Length is already set, then calls to Write on gzip 134 // will fail to set the Content-Length header since its already set 135 // See: https://github.com/golang/go/issues/14975. 136 w.Header().Del(contentLength) 137 138 // Write the header to gzip response. 139 if w.code != 0 { 140 w.ResponseWriter.WriteHeader(w.code) 141 } 142 143 // Initialize the GZIP response. 144 w.init() 145 146 // Flush the buffer into the gzip response. 147 n, err := w.gw.Write(w.buf) 148 149 // This should never happen (per io.Writer docs), but if the write didn't 150 // accept the entire buffer but returned no specific error, we have no clue 151 // what's going on, so abort just to be safe. 152 if err == nil && n < len(w.buf) { 153 return io.ErrShortWrite 154 } 155 156 w.buf = nil 157 return err 158} 159 160// WriteHeader just saves the response code until close or GZIP effective writes. 161func (w *GzipResponseWriter) WriteHeader(code int) { 162 if w.code == 0 { 163 w.code = code 164 } 165} 166 167// init graps a new gzip writer from the gzipWriterPool and writes the correct 168// content encoding header. 169func (w *GzipResponseWriter) init() { 170 // Bytes written during ServeHTTP are redirected to this gzip writer 171 // before being written to the underlying response. 172 gzw := gzipWriterPools[w.index].Get().(*gzip.Writer) 173 gzw.Reset(w.ResponseWriter) 174 w.gw = gzw 175} 176 177// Close will close the gzip.Writer and will put it back in the gzipWriterPool. 178func (w *GzipResponseWriter) Close() error { 179 if w.gw == nil { 180 // Gzip not trigged yet, write out regular response. 181 if w.code != 0 { 182 w.ResponseWriter.WriteHeader(w.code) 183 } 184 if w.buf != nil { 185 _, writeErr := w.ResponseWriter.Write(w.buf) 186 // Returns the error if any at write. 187 if writeErr != nil { 188 return fmt.Errorf("gziphandler: write to regular responseWriter at close gets error: %q", writeErr.Error()) 189 } 190 } 191 return nil 192 } 193 194 err := w.gw.Close() 195 gzipWriterPools[w.index].Put(w.gw) 196 w.gw = nil 197 return err 198} 199 200// Flush flushes the underlying *gzip.Writer and then the underlying 201// http.ResponseWriter if it is an http.Flusher. This makes GzipResponseWriter 202// an http.Flusher. 203func (w *GzipResponseWriter) Flush() { 204 if w.gw == nil { 205 // Only flush once startGzip has been called. 206 // 207 // Flush is thus a no-op until the written body 208 // exceeds minSize. 209 return 210 } 211 212 w.gw.Flush() 213 214 if fw, ok := w.ResponseWriter.(http.Flusher); ok { 215 fw.Flush() 216 } 217} 218 219// Hijack implements http.Hijacker. If the underlying ResponseWriter is a 220// Hijacker, its Hijack method is returned. Otherwise an error is returned. 221func (w *GzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 222 if hj, ok := w.ResponseWriter.(http.Hijacker); ok { 223 return hj.Hijack() 224 } 225 return nil, nil, fmt.Errorf("http.Hijacker interface is not supported") 226} 227 228// verify Hijacker interface implementation 229var _ http.Hijacker = &GzipResponseWriter{} 230 231// MustNewGzipLevelHandler behaves just like NewGzipLevelHandler except that in 232// an error case it panics rather than returning an error. 233func MustNewGzipLevelHandler(level int) func(http.Handler) http.Handler { 234 wrap, err := NewGzipLevelHandler(level) 235 if err != nil { 236 panic(err) 237 } 238 return wrap 239} 240 241// NewGzipLevelHandler returns a wrapper function (often known as middleware) 242// which can be used to wrap an HTTP handler to transparently gzip the response 243// body if the client supports it (via the Accept-Encoding header). Responses will 244// be encoded at the given gzip compression level. An error will be returned only 245// if an invalid gzip compression level is given, so if one can ensure the level 246// is valid, the returned error can be safely ignored. 247func NewGzipLevelHandler(level int) (func(http.Handler) http.Handler, error) { 248 return NewGzipLevelAndMinSize(level, DefaultMinSize) 249} 250 251// NewGzipLevelAndMinSize behave as NewGzipLevelHandler except it let the caller 252// specify the minimum size before compression. 253func NewGzipLevelAndMinSize(level, minSize int) (func(http.Handler) http.Handler, error) { 254 return GzipHandlerWithOpts(CompressionLevel(level), MinSize(minSize)) 255} 256 257func GzipHandlerWithOpts(opts ...option) (func(http.Handler) http.Handler, error) { 258 c := &config{ 259 level: gzip.DefaultCompression, 260 minSize: DefaultMinSize, 261 } 262 263 for _, o := range opts { 264 o(c) 265 } 266 267 if err := c.validate(); err != nil { 268 return nil, err 269 } 270 271 return func(h http.Handler) http.Handler { 272 index := poolIndex(c.level) 273 274 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 275 w.Header().Add(vary, acceptEncoding) 276 if acceptsGzip(r) { 277 gw := &GzipResponseWriter{ 278 ResponseWriter: w, 279 index: index, 280 minSize: c.minSize, 281 contentTypes: c.contentTypes, 282 } 283 defer gw.Close() 284 285 if _, ok := w.(http.CloseNotifier); ok { 286 gwcn := GzipResponseWriterWithCloseNotify{gw} 287 h.ServeHTTP(gwcn, r) 288 } else { 289 h.ServeHTTP(gw, r) 290 } 291 292 } else { 293 h.ServeHTTP(w, r) 294 } 295 }) 296 }, nil 297} 298 299// Used for functional configuration. 300type config struct { 301 minSize int 302 level int 303 contentTypes []string 304} 305 306func (c *config) validate() error { 307 if c.level != gzip.DefaultCompression && (c.level < gzip.BestSpeed || c.level > gzip.BestCompression) { 308 return fmt.Errorf("invalid compression level requested: %d", c.level) 309 } 310 311 if c.minSize < 0 { 312 return fmt.Errorf("minimum size must be more than zero") 313 } 314 315 return nil 316} 317 318type option func(c *config) 319 320func MinSize(size int) option { 321 return func(c *config) { 322 c.minSize = size 323 } 324} 325 326func CompressionLevel(level int) option { 327 return func(c *config) { 328 c.level = level 329 } 330} 331 332func ContentTypes(types []string) option { 333 return func(c *config) { 334 c.contentTypes = []string{} 335 for _, v := range types { 336 c.contentTypes = append(c.contentTypes, strings.ToLower(v)) 337 } 338 } 339} 340 341// GzipHandler wraps an HTTP handler, to transparently gzip the response body if 342// the client supports it (via the Accept-Encoding header). This will compress at 343// the default compression level. 344func GzipHandler(h http.Handler) http.Handler { 345 wrapper, _ := NewGzipLevelHandler(gzip.DefaultCompression) 346 return wrapper(h) 347} 348 349// acceptsGzip returns true if the given HTTP request indicates that it will 350// accept a gzipped response. 351func acceptsGzip(r *http.Request) bool { 352 acceptedEncodings, _ := parseEncodings(r.Header.Get(acceptEncoding)) 353 return acceptedEncodings["gzip"] > 0.0 354} 355 356// returns true if we've been configured to compress the specific content type. 357func handleContentType(contentTypes []string, w http.ResponseWriter) bool { 358 // If contentTypes is empty we handle all content types. 359 if len(contentTypes) == 0 { 360 return true 361 } 362 363 ct := strings.ToLower(w.Header().Get(contentType)) 364 for _, c := range contentTypes { 365 if c == ct { 366 return true 367 } 368 } 369 370 return false 371} 372 373// parseEncodings attempts to parse a list of codings, per RFC 2616, as might 374// appear in an Accept-Encoding header. It returns a map of content-codings to 375// quality values, and an error containing the errors encountered. It's probably 376// safe to ignore those, because silently ignoring errors is how the internet 377// works. 378// 379// See: http://tools.ietf.org/html/rfc2616#section-14.3. 380func parseEncodings(s string) (codings, error) { 381 c := make(codings) 382 var e []string 383 384 for _, ss := range strings.Split(s, ",") { 385 coding, qvalue, err := parseCoding(ss) 386 387 if err != nil { 388 e = append(e, err.Error()) 389 } else { 390 c[coding] = qvalue 391 } 392 } 393 394 // TODO (adammck): Use a proper multi-error struct, so the individual errors 395 // can be extracted if anyone cares. 396 if len(e) > 0 { 397 return c, fmt.Errorf("errors while parsing encodings: %s", strings.Join(e, ", ")) 398 } 399 400 return c, nil 401} 402 403// parseCoding parses a single conding (content-coding with an optional qvalue), 404// as might appear in an Accept-Encoding header. It attempts to forgive minor 405// formatting errors. 406func parseCoding(s string) (coding string, qvalue float64, err error) { 407 for n, part := range strings.Split(s, ";") { 408 part = strings.TrimSpace(part) 409 qvalue = DefaultQValue 410 411 if n == 0 { 412 coding = strings.ToLower(part) 413 } else if strings.HasPrefix(part, "q=") { 414 qvalue, err = strconv.ParseFloat(strings.TrimPrefix(part, "q="), 64) 415 416 if qvalue < 0.0 { 417 qvalue = 0.0 418 } else if qvalue > 1.0 { 419 qvalue = 1.0 420 } 421 } 422 } 423 424 if coding == "" { 425 err = fmt.Errorf("empty content-coding") 426 } 427 428 return 429} 430