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 1 commit
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
Prev Previous commit
Next Next commit
Added MinSizeFunc and streamline names.
  • Loading branch information
blaubaer committed Dec 18, 2023
commit cb5de65bd5166af27e654a9c20db0920b7348c44
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