Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split adapter and wrapper into different entities #57

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
*.swp
/vendor/
/vendor/
/.idea/
2 changes: 1 addition & 1 deletion accepts.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func parseEncodings(vv []string) codings {
return c
}

// parseCoding parses a single conding (content-coding with an optional qvalue),
// parseCoding parses a single coding (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) {
Expand Down
214 changes: 21 additions & 193 deletions adapter.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
package httpcompression // import "github.com/CAFxX/httpcompression"
package httpcompression

import (
"compress/gzip"
"fmt"
"net/http"
"strings"
"sync"

"github.com/CAFxX/httpcompression/contrib/andybalholm/brotli"
cgzip "github.com/CAFxX/httpcompression/contrib/compress/gzip"
"github.com/CAFxX/httpcompression/contrib/compress/zlib"
"github.com/CAFxX/httpcompression/contrib/klauspost/zstd"
)

const (
Expand All @@ -23,102 +15,45 @@ const (
_range = "Range"
)

type codings map[string]float64

const (
// DefaultMinSize is the default minimum response body size for which we enable compression.
//
// 200 is a somewhat arbitrary number; in experiments compressing short text/markup-like sequences
// with different compressors we saw that sequences shorter that ~180 the output generated by the
// compressor would sometime be larger than the input.
// This default may change between versions.
// In general there can be no one-size-fits-all value: you will want to measure if a different
// minimum size improves end-to-end performance for your workloads.
DefaultMinSize = 200
)

// Adapter returns a HTTP handler wrapping function (a.k.a. middleware)
// Adapter returns an HTTP handler wrapping function (a.k.a. middleware)
// which can be used to wrap an HTTP handler to transparently compress the response
// body if the client supports it (via the Accept-Encoding header).
// It is possible to pass one or more options to modify the middleware configuration.
// If no options are provided, no compressors are enabled and therefore the adapter
// is a no-op.
// An error will be returned if invalid options are given.
func Adapter(opts ...Option) (func(http.Handler) http.Handler, error) {
c := config{
prefer: PreferServer,
compressor: comps{},
}
for _, o := range opts {
err := o(&c)
if err != nil {
return nil, err
}
f, err := NewResponseWriterFactory(opts...)
if err != nil {
return nil, err
}
return adapter(f)
}

if len(c.compressor) == 0 {
func adapter(f *ResponseWriterFactoryFactory) (func(http.Handler) http.Handler, error) {
if f.AmountOfCompressors() == 0 {
// No compressors have been configured, so there is no useful work
// that this adapter can do.
return func(h http.Handler) http.Handler {
return h
}, nil
}

bufPool := &sync.Pool{}
writerPool := &sync.Pool{}

return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
addVaryHeader(w.Header(), acceptEncoding)

accept := parseEncodings(r.Header.Values(acceptEncoding))
common := acceptedCompression(accept, c.compressor)
if len(common) == 0 {
h.ServeHTTP(w, r)
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
ww, finalizer, err := f.Create(rw, req)
if err != nil {
f.config.handleError(rw, req, err)
return
}

// We do not handle range requests when compression is used, as the
// range specified applies to the compressed data, not to the uncompressed one.
// So we would need to (1) ensure that compressors are deterministic and (2)
// generate the whole uncompressed response anyway, compress it, and then discard
// the bits outside of the range.
// Let's keep it simple, and simply ignore completely the range header.
// We also need to remove the Accept: Range header from any response that is
// compressed; this is done in the ResponseWriter.
// See https://github.com/nytimes/gziphandler/issues/83.
r.Header.Del(_range)

gw, _ := writerPool.Get().(*compressWriter)
if gw == nil {
gw = &compressWriter{}
}
*gw = compressWriter{
ResponseWriter: w,
config: c,
accept: accept,
common: common,
pool: bufPool,
}
defer func() {
// Important: gw.Close() must be called *always*, as this will
// in turn Close() the compressor. This is important because
// it is guaranteed by the CompressorProvider interface, and
// because some compressors may be implemented via cgo, and they
// may rely on Close() being called to release memory resources.
// TODO: expose the error
_ = gw.Close() // expose the error
*gw = compressWriter{}
writerPool.Put(gw)
if err := finalizer(); err != nil {
f.config.handleError(rw, req, err)
}
}()

if _, ok := w.(http.CloseNotifier); ok {
w = compressWriterWithCloseNotify{gw}
} else {
w = gw
}

h.ServeHTTP(w, r)
h.ServeHTTP(ww, req)
})
}, nil
}
Expand All @@ -133,122 +68,15 @@ func addVaryHeader(h http.Header, value string) {
}

// DefaultAdapter is like Adapter, but it includes sane defaults for general usage.
// Currently the defaults enable gzip and brotli compression, and set a minimum body size
// Currently, the defaults enable gzip and brotli compression, and set a minimum body size
// of 200 bytes.
// The provided opts override the defaults.
// The defaults are not guaranteed to remain constant over time: if you want to avoid this
// use Adapter directly.
func DefaultAdapter(opts ...Option) (func(http.Handler) http.Handler, error) {
defaults := []Option{
DeflateCompressionLevel(zlib.DefaultCompression),
GzipCompressionLevel(gzip.DefaultCompression),
BrotliCompressionLevel(brotli.DefaultCompression),
defaultZstandardCompressor(),
MinSize(DefaultMinSize),
}
opts = append(defaults, opts...)
return Adapter(opts...)
}

// Used for functional configuration.
type config struct {
minSize int // Specifies the minimum response size to gzip. If the response length is bigger than this value, it is compressed.
contentTypes []parsedContentType // Only compress if the response is one of these content-types. All are accepted if empty.
blacklist bool
prefer PreferType
compressor comps
}

type comps map[string]comp

type comp struct {
comp CompressorProvider
priority int
}

// Option can be passed to Handler to control its configuration.
type Option func(c *config) error

// MinSize is an option that controls the minimum size of payloads that
// should be compressed. The default is DefaultMinSize.
func MinSize(size int) Option {
return func(c *config) error {
if size < 0 {
return fmt.Errorf("minimum size can not be negative: %d", size)
}
c.minSize = size
return nil
}
}

// DeflateCompressionLevel is an option that controls the Deflate compression
// level to be used when compressing payloads.
// The default is flate.DefaultCompression.
func DeflateCompressionLevel(level int) Option {
c, err := zlib.New(zlib.Options{Level: level})
if err != nil {
return errorOption(err)
}
return DeflateCompressor(c)
}

// GzipCompressionLevel is an option that controls the Gzip compression
// level to be used when compressing payloads.
// The default is gzip.DefaultCompression.
func GzipCompressionLevel(level int) Option {
c, err := NewDefaultGzipCompressor(level)
if err != nil {
return errorOption(err)
}
return GzipCompressor(c)
}

// BrotliCompressionLevel is an option that controls the Brotli compression
// level to be used when compressing payloads.
// The default is 3 (the same default used in the reference brotli C
// implementation).
func BrotliCompressionLevel(level int) Option {
c, err := brotli.New(brotli.Options{Quality: level})
f, err := NewDefaultResponseWriterFactory(opts...)
if err != nil {
return errorOption(err)
}
return BrotliCompressor(c)
}

// DeflateCompressor is an option to specify a custom compressor factory for Deflate.
func DeflateCompressor(g CompressorProvider) Option {
return Compressor(zlib.Encoding, -300, g)
}

// GzipCompressor is an option to specify a custom compressor factory for Gzip.
func GzipCompressor(g CompressorProvider) Option {
return Compressor(cgzip.Encoding, -200, g)
}

// BrotliCompressor is an option to specify a custom compressor factory for Brotli.
func BrotliCompressor(b CompressorProvider) Option {
return Compressor(brotli.Encoding, -100, b)
}

// ZstandardCompressor is an option to specify a custom compressor factory for Zstandard.
func ZstandardCompressor(b CompressorProvider) Option {
return Compressor(zstd.Encoding, -50, b)
}

func NewDefaultGzipCompressor(level int) (CompressorProvider, error) {
return cgzip.New(cgzip.Options{Level: level})
}

func defaultZstandardCompressor() Option {
zstdComp, err := zstd.New()
if err != nil {
return errorOption(fmt.Errorf("initializing zstd compressor: %w", err))
}
return ZstandardCompressor(zstdComp)
}

func errorOption(err error) Option {
return func(_ *config) error {
return err
return nil, err
}
return adapter(f)
}
Loading