Update github.com/NYTimes/gziphandler
This commit is contained in:
296
vendor/github.com/NYTimes/gziphandler/gzip.go
generated
vendored
296
vendor/github.com/NYTimes/gziphandler/gzip.go
generated
vendored
@@ -1,10 +1,11 @@
|
||||
package gziphandler
|
||||
package gziphandler // import "github.com/NYTimes/gziphandler"
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -28,9 +29,11 @@ const (
|
||||
// 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
|
||||
// DefaultMinSize is the default minimum size until we enable gzip compression.
|
||||
// 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
|
||||
)
|
||||
|
||||
// gzipWriterPools stores a sync.Pool for each compression level for reuse of
|
||||
@@ -80,40 +83,71 @@ 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.
|
||||
ignore bool // If true, then we immediately passthru writes to the underlying ResponseWriter.
|
||||
|
||||
contentTypes []parsedContentType // Only compress if the response is one of these content-types. All are accepted if empty.
|
||||
}
|
||||
|
||||
type GzipResponseWriterWithCloseNotify struct {
|
||||
*GzipResponseWriter
|
||||
}
|
||||
|
||||
func (w GzipResponseWriterWithCloseNotify) CloseNotify() <-chan bool {
|
||||
return w.ResponseWriter.(http.CloseNotifier).CloseNotify()
|
||||
}
|
||||
|
||||
// Write appends data to the gzip writer.
|
||||
func (w *GzipResponseWriter) Write(b []byte) (int, error) {
|
||||
// If content type is not set.
|
||||
if _, ok := w.Header()[contentType]; !ok {
|
||||
// It infer it from the uncompressed body.
|
||||
w.Header().Set(contentType, http.DetectContentType(b))
|
||||
}
|
||||
|
||||
// GZIP responseWriter is initialized. Use the GZIP responseWriter.
|
||||
if w.gw != nil {
|
||||
n, err := w.gw.Write(b)
|
||||
return n, err
|
||||
return w.gw.Write(b)
|
||||
}
|
||||
|
||||
// If we have already decided not to use GZIP, immediately passthrough.
|
||||
if w.ignore {
|
||||
return w.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
// Save the write into a buffer for later use in GZIP responseWriter (if content is long enough) or at close with regular responseWriter.
|
||||
// On the first write, w.buf changes from nil to a valid slice
|
||||
w.buf = append(w.buf, b...)
|
||||
|
||||
// If the global writes are bigger than the minSize, compression is enable.
|
||||
if len(w.buf) >= w.minSize {
|
||||
err := w.startGzip()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
var (
|
||||
cl, _ = strconv.Atoi(w.Header().Get(contentLength))
|
||||
ct = w.Header().Get(contentType)
|
||||
ce = w.Header().Get(contentEncoding)
|
||||
)
|
||||
// Only continue if they didn't already choose an encoding or a known unhandled content length or type.
|
||||
if ce == "" && (cl == 0 || cl >= w.minSize) && (ct == "" || handleContentType(w.contentTypes, ct)) {
|
||||
// If the current buffer is less than minSize and a Content-Length isn't set, then wait until we have more data.
|
||||
if len(w.buf) < w.minSize && cl == 0 {
|
||||
return len(b), nil
|
||||
}
|
||||
// If the Content-Length is larger than minSize or the current buffer is larger than minSize, then continue.
|
||||
if cl >= w.minSize || len(w.buf) >= w.minSize {
|
||||
// If a Content-Type wasn't specified, infer it from the current buffer.
|
||||
if ct == "" {
|
||||
ct = http.DetectContentType(w.buf)
|
||||
w.Header().Set(contentType, ct)
|
||||
}
|
||||
// If the Content-Type is acceptable to GZIP, initialize the GZIP writer.
|
||||
if handleContentType(w.contentTypes, ct) {
|
||||
if err := w.startGzip(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we got here, we should not GZIP this response.
|
||||
if err := w.startPlain(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
// startGzip initialize any GZIP specific informations.
|
||||
// startGzip initializes a GZIP writer and writes the buffer.
|
||||
func (w *GzipResponseWriter) startGzip() error {
|
||||
|
||||
// Set the GZIP header.
|
||||
w.Header().Set(contentEncoding, "gzip")
|
||||
|
||||
@@ -125,28 +159,57 @@ func (w *GzipResponseWriter) startGzip() error {
|
||||
// Write the header to gzip response.
|
||||
if w.code != 0 {
|
||||
w.ResponseWriter.WriteHeader(w.code)
|
||||
// Ensure that no other WriteHeader's happen
|
||||
w.code = 0
|
||||
}
|
||||
|
||||
// Initialize the GZIP response.
|
||||
w.init()
|
||||
// Initialize and flush the buffer into the gzip response if there are any bytes.
|
||||
// If there aren't any, we shouldn't initialize it yet because on Close it will
|
||||
// write the gzip header even if nothing was ever written.
|
||||
if len(w.buf) > 0 {
|
||||
// Initialize the GZIP response.
|
||||
w.init()
|
||||
n, err := w.gw.Write(w.buf)
|
||||
|
||||
// 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) {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// startPlain writes to sent bytes and buffer the underlying ResponseWriter without gzip.
|
||||
func (w *GzipResponseWriter) startPlain() error {
|
||||
if w.code != 0 {
|
||||
w.ResponseWriter.WriteHeader(w.code)
|
||||
// Ensure that no other WriteHeader's happen
|
||||
w.code = 0
|
||||
}
|
||||
w.ignore = true
|
||||
// If Write was never called then don't call Write on the underlying ResponseWriter.
|
||||
if w.buf == nil {
|
||||
return nil
|
||||
}
|
||||
n, err := w.ResponseWriter.Write(w.buf)
|
||||
w.buf = nil
|
||||
// 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
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
|
||||
w.buf = nil
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteHeader just saves the response code until close or GZIP effective writes.
|
||||
func (w *GzipResponseWriter) WriteHeader(code int) {
|
||||
w.code = code
|
||||
if w.code == 0 {
|
||||
w.code = code
|
||||
}
|
||||
}
|
||||
|
||||
// init graps a new gzip writer from the gzipWriterPool and writes the correct
|
||||
@@ -161,21 +224,20 @@ func (w *GzipResponseWriter) init() {
|
||||
|
||||
// Close will close the gzip.Writer and will put it back in the gzipWriterPool.
|
||||
func (w *GzipResponseWriter) Close() error {
|
||||
if w.gw == nil {
|
||||
// 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())
|
||||
}
|
||||
}
|
||||
if w.ignore {
|
||||
return nil
|
||||
}
|
||||
|
||||
if w.gw == nil {
|
||||
// GZIP not triggered yet, write out regular response.
|
||||
err := w.startPlain()
|
||||
// Returns the error if any at write.
|
||||
if err != nil {
|
||||
err = fmt.Errorf("gziphandler: write to regular responseWriter at close gets error: %q", err.Error())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
err := w.gw.Close()
|
||||
gzipWriterPools[w.index].Put(w.gw)
|
||||
w.gw = nil
|
||||
@@ -186,6 +248,14 @@ func (w *GzipResponseWriter) Close() error {
|
||||
// http.ResponseWriter if it is an http.Flusher. This makes GzipResponseWriter
|
||||
// an http.Flusher.
|
||||
func (w *GzipResponseWriter) Flush() {
|
||||
if w.gw == nil && !w.ignore {
|
||||
// Only flush once startGzip or startPlain has been called.
|
||||
//
|
||||
// Flush is thus a no-op until we're certain whether a plain
|
||||
// or gzipped response will be served.
|
||||
return
|
||||
}
|
||||
|
||||
if w.gw != nil {
|
||||
w.gw.Flush()
|
||||
}
|
||||
@@ -230,27 +300,44 @@ func NewGzipLevelHandler(level int) (func(http.Handler) http.Handler, error) {
|
||||
// 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) {
|
||||
if level != gzip.DefaultCompression && (level < gzip.BestSpeed || level > gzip.BestCompression) {
|
||||
return nil, fmt.Errorf("invalid compression level requested: %d", level)
|
||||
return GzipHandlerWithOpts(CompressionLevel(level), MinSize(minSize))
|
||||
}
|
||||
|
||||
func GzipHandlerWithOpts(opts ...option) (func(http.Handler) http.Handler, error) {
|
||||
c := &config{
|
||||
level: gzip.DefaultCompression,
|
||||
minSize: DefaultMinSize,
|
||||
}
|
||||
if minSize < 0 {
|
||||
return nil, fmt.Errorf("minimum size must be more than zero")
|
||||
|
||||
for _, o := range opts {
|
||||
o(c)
|
||||
}
|
||||
|
||||
if err := c.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return func(h http.Handler) http.Handler {
|
||||
index := poolIndex(level)
|
||||
index := poolIndex(c.level)
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add(vary, acceptEncoding)
|
||||
|
||||
if acceptsGzip(r) {
|
||||
gw := &GzipResponseWriter{
|
||||
ResponseWriter: w,
|
||||
index: index,
|
||||
minSize: minSize,
|
||||
minSize: c.minSize,
|
||||
contentTypes: c.contentTypes,
|
||||
}
|
||||
defer gw.Close()
|
||||
|
||||
h.ServeHTTP(gw, r)
|
||||
if _, ok := w.(http.CloseNotifier); ok {
|
||||
gwcn := GzipResponseWriterWithCloseNotify{gw}
|
||||
h.ServeHTTP(gwcn, r)
|
||||
} else {
|
||||
h.ServeHTTP(gw, r)
|
||||
}
|
||||
|
||||
} else {
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
@@ -258,6 +345,98 @@ func NewGzipLevelAndMinSize(level, minSize int) (func(http.Handler) http.Handler
|
||||
}, 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 []parsedContentType
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = []parsedContentType{}
|
||||
for _, v := range types {
|
||||
mediaType, params, err := mime.ParseMediaType(v)
|
||||
if err == nil {
|
||||
c.contentTypes = append(c.contentTypes, parsedContentType{mediaType, params})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -273,6 +452,27 @@ func acceptsGzip(r *http.Request) bool {
|
||||
return acceptedEncodings["gzip"] > 0.0
|
||||
}
|
||||
|
||||
// returns true if we've been configured to compress the specific content type.
|
||||
func handleContentType(contentTypes []parsedContentType, ct string) bool {
|
||||
// If contentTypes is empty we handle all content types.
|
||||
if len(contentTypes) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
mediaType, params, err := mime.ParseMediaType(ct)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, c := range contentTypes {
|
||||
if c.equals(mediaType, params) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 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
|
||||
|
Reference in New Issue
Block a user