Skip to content

Commit

Permalink
Added MinSizeFunc and streamline names.
Browse files Browse the repository at this point in the history
  • Loading branch information
blaubaer committed Dec 18, 2023
1 parent 0f838c1 commit cb5de65
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 77 deletions.
20 changes: 10 additions & 10 deletions adapter.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package httpcompression // import "github.com/CAFxX/httpcompression"
package httpcompression

import (
"net/http"
Expand All @@ -23,15 +23,15 @@ const (
// is a no-op.
// An error will be returned if invalid options are given.
func Adapter(opts ...Option) (func(http.Handler) http.Handler, error) {
wrapper, err := NewResponseWriterWrapper(opts...)
f, err := NewResponseWriterFactory(opts...)
if err != nil {
return nil, err
}
return adapter(wrapper)
return adapter(f)
}

func adapter(wrapper *ResponseWriterWrapper) (func(http.Handler) http.Handler, error) {
if wrapper.AmountOfCompressors() == 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 {
Expand All @@ -41,15 +41,15 @@ func adapter(wrapper *ResponseWriterWrapper) (func(http.Handler) http.Handler, e

return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
ww, _, finalizer, err := wrapper.Wrap(rw, req)
ww, finalizer, err := f.Create(rw, req)
if err != nil {
wrapper.config.handleError(rw, req, err)
f.config.handleError(rw, req, err)
return
}

defer func() {
if err := finalizer(); err != nil {
wrapper.config.handleError(rw, req, err)
f.config.handleError(rw, req, err)
}
}()

Expand All @@ -74,9 +74,9 @@ func addVaryHeader(h http.Header, value string) {
// 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) {
wrapper, err := NewDefaultResponseWriterWrapper(opts...)
f, err := NewDefaultResponseWriterFactory(opts...)
if err != nil {
return nil, err
}
return adapter(wrapper)
return adapter(f)
}
65 changes: 65 additions & 0 deletions adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,71 @@ func TestGzipHandlerMinSize(t *testing.T) {
}
}

func TestGzipHandlerMinSizeRequestFunc(t *testing.T) {
t.Parallel()

responseLength := 0
b := []byte{'x'}

adapter, _ := DefaultAdapter(MinSizeRequestFunc(func(req *http.Request) (int, error) {
return 128, nil
}))
handler := adapter(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
// Write responses one byte at a time to ensure that the flush
// mechanism, if used, is working properly.
for i := 0; i < responseLength; i++ {
n, err := w.Write(b)
assert.Equal(t, 1, n)
assert.Nil(t, err)
}
},
))

r, _ := http.NewRequest("GET", "/whatever", &bytes.Buffer{})
r.Header.Add("Accept-Encoding", "gzip")

// Short response is not compressed
responseLength = 127
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Result().Header.Get(contentEncoding) == "gzip" {
t.Error("Expected uncompressed response, got compressed")
}

// Long response is compressed
responseLength = 128
w = httptest.NewRecorder()
handler.ServeHTTP(w, r)
if w.Result().Header.Get(contentEncoding) != "gzip" {
t.Error("Expected compressed response, got uncompressed")
}
}

func TestFailGzipHandlerMinSizeRequestFunc(t *testing.T) {
t.Parallel()

expectedError := errors.New("expected")
var actualError error
adapter, _ := DefaultAdapter(
MinSizeRequestFunc(func(req *http.Request) (int, error) {
return 0, expectedError
}),
ErrorHandler(func(_ http.ResponseWriter, _ *http.Request, err error) {
actualError = err
}),
)

handler := adapter(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
r, _ := http.NewRequest("GET", "/whatever", &bytes.Buffer{})
r.Header.Add("Accept-Encoding", "gzip")
w := httptest.NewRecorder()

handler.ServeHTTP(w, r)

assert.ErrorIs(t, actualError, expectedError)
}

type panicOnSecondWriteHeaderWriter struct {
http.ResponseWriter
headerWritten bool
Expand Down
40 changes: 37 additions & 3 deletions option.go → config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ import (
"net/http"
)

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
)

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

Expand All @@ -20,11 +32,30 @@ func MinSize(size int) Option {
if size < 0 {
return fmt.Errorf("minimum size can not be negative: %d", size)
}
if c.minSizeFunc != nil {
return fmt.Errorf("cannot use MinSize and MinSizeRequestFunc together")
}
c.minSize = size
return nil
}
}

// MinSizeRequestFunc is an option that controls the minimum size of payloads that
// should be compressed. The provided func can select this minimum based on
// the provided http.Request. The default is DefaultMinSize.
func MinSizeRequestFunc(f func(*http.Request) (int, error)) Option {
return func(c *config) error {
if f == nil {
return fmt.Errorf("there was no minSizeFunc provided")
}
if c.minSize > 0 {
return fmt.Errorf("cannot use MinSize and MinSizeRequestFunc together")
}
c.minSizeFunc = f
return nil
}
}

// DeflateCompressionLevel is an option that controls the Deflate compression
// level to be used when compressing payloads.
// The default is flate.DefaultCompression.
Expand Down Expand Up @@ -97,6 +128,8 @@ func errorOption(err error) Option {
}
}

// ErrorHandler defines what should happen if an unexpected error happens
// within the httpcompression execution chain.
func ErrorHandler(handler func(w http.ResponseWriter, r *http.Request, err error)) Option {
return func(c *config) error {
c.errorHandler = handler
Expand All @@ -106,15 +139,16 @@ func ErrorHandler(handler func(w http.ResponseWriter, r *http.Request, err error

// 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.
minSize int // Specifies the minimum response size to gzip. If the response length is bigger than this value, it is compressed.
minSizeFunc func(r *http.Request) (int, error) // Similar to minSize but selects the value based on given request.
contentTypes []parsedContentType // Only compress if the response is one of these content-types. All are accepted if empty.
blacklist bool
prefer PreferType
compressor comps
errorHandler func(w http.ResponseWriter, r *http.Request, err error)
}

func (c config) handleError(w http.ResponseWriter, r *http.Request, err error) {
func (c *config) handleError(w http.ResponseWriter, r *http.Request, err error) {
if c.errorHandler != nil {
c.errorHandler(w, r, err)
} else {
Expand Down
28 changes: 18 additions & 10 deletions response_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ var (
type compressWriter struct {
http.ResponseWriter

config *config
accept codings
common []string
pool *sync.Pool // pool of buffers (buf []byte); max size of each buf is maxBuf
config *config
minSize int
accept codings
common []string
pool *sync.Pool // pool of buffers (buf []byte); max size of each buf is maxBuf

w io.Writer
enc string
Expand Down Expand Up @@ -58,15 +59,22 @@ var (

const maxBuf = 1 << 16 // maximum size of recycled buffer

func (w *compressWriter) configure(rw http.ResponseWriter, accept codings, common []string) {
func (w *compressWriter) configure(
rw http.ResponseWriter,
minSize int,
accept codings,
common []string,
) {
w.ResponseWriter = rw
w.minSize = minSize
w.accept = accept
w.common = common
w.w = nil
}

func (w *compressWriter) clean() {
w.ResponseWriter = nil
w.minSize = 0
w.accept = nil
w.common = nil
w.w = nil
Expand All @@ -91,8 +99,8 @@ func (w *compressWriter) Write(b []byte) (int, error) {
// Fast path: we have enough information to know whether we will compress
// or not this response from the first write, so we don't need to buffer
// writes to defer the decision until we have more data.
if w.buf == nil && (ct != "" || len(w.config.contentTypes) == 0) && (cl > 0 || len(b) >= w.config.minSize) {
if ce == "" && (cl >= w.config.minSize || len(b) >= w.config.minSize) && handleContentType(ct, w.config.contentTypes, w.config.blacklist) {
if w.buf == nil && (ct != "" || len(w.config.contentTypes) == 0) && (cl > 0 || len(b) >= w.minSize) {
if ce == "" && (cl >= w.minSize || len(b) >= w.minSize) && handleContentType(ct, w.config.contentTypes, w.config.blacklist) {
enc := preferredEncoding(w.accept, w.config.compressor, w.common, w.config.prefer)
if err := w.startCompress(enc, b); err != nil {
return 0, err
Expand All @@ -113,13 +121,13 @@ func (w *compressWriter) Write(b []byte) (int, error) {
*w.buf = append(*w.buf, b...)

// Only continue if they didn't already choose an encoding or a known unhandled content length or type.
if ce == "" && (cl == 0 || cl >= w.config.minSize) && (ct == "" || handleContentType(ct, w.config.contentTypes, w.config.blacklist)) {
if ce == "" && (cl == 0 || cl >= w.minSize) && (ct == "" || handleContentType(ct, w.config.contentTypes, w.config.blacklist)) {
// 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.config.minSize && cl == 0 {
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.config.minSize || len(*w.buf) >= w.config.minSize {
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)
Expand Down
Loading

0 comments on commit cb5de65

Please sign in to comment.