Documentation
¶
Overview ¶
package compresshttp provides HTTP client and server middleware for transparent compression and decompression of HTTP request and response bodies using the 'Accept-Encoding' and 'Content-Encoding' headers.
It is meant to be a 'one-size fits most' solution for HTTP compression needs in Go applications that provides fairly good performance with minimal configuration. In general, this should be easier and more performant than most solutions at the expense of a slightly wider API surface area.
Supported compression methods ¶
Compresshttp supports all major compression methods commonly used in HTTP: see https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Encoding.
- GZIP (stdlib compress/gzip for small and medium bodies, klauspost/pgzip for large bodies)
- ZSTD (klauspost/compress/zstd for writing, stdlibzstd for reading)
- Deflate (stdlib compress/flate)
- Brotli (andybalholm/brotli)
- Snappy (golang/snappy)
Overview ¶
Use ServerMiddleware to accept compressed http requests and send compressed responses to clients that send 'Accept-Encoding' headers.
Use ClientAcceptAllMiddleware to accept compressed responses from servers that send compressed responses.
[Optional]: Use up to one of the ClientEncode...Middleware functions to compress request bodies sent to servers using the specified compression method. That is, use ClientEncodeGZIPMiddleware to compress request bodies using GZIP, ClientEncodeZSTDMiddleware to compress request bodies using ZSTD, etc.
Configuration ¶
All configuration is optional: pass nil to automatically use the same configuration as you'd get from DefaultServerConfig() or DefaultClientConfig().
Teachability ¶
This package is primarily designed to be practically useful, but has a secondary purpose: teaching journeyman Go programmers how to design and implement a good library. I intend to write some articles and publish a video about library design using this package as an example.
As such, it is extremely heavily commented, explaining the reasoning behind design decisions, trade-offs considered, and weaknesses of various designs, even in areas like tests and error handling. I strongly encourage you to read the source code and documentation.
Prototypes ¶
- This package contains prototype implementations in the prototype/alpha and prototype/beta sub-packages; these are not meant for use but instead to illustrate the evolution of the design
- The alpha package contains a very simple GZIP-only implementation that is easy to understand completely, but has many limitations.
- The beta package contains a more complete implementation that supports multiple compression methods and has fewer limitations, but is more complex.
Note: I "filled in" these prototypes AFTER designing and implementing this package, so they do not represent the actual evolution: for one, there was a 'dead end' of overly complex designs somewhere between alpha and beta that I have not included here. I should have committed more often during the design process!
Example ¶
Basic usage: add both client and server middleware. This will automatically and transparently add the 'Accept-Encoding' headers to the HTTP request, which will be picked up by the server. The server will then respond with a compressed response body, which will be transparently decompressed by the client. From the user's perspective, everything 'just works'.
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
_ "embed"
"gitlab.com/efronlicht/compresshttp"
)
func main() {
// handler writes the Gettysburg Address as a response body
var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(gettysburgSentence))
})
// add default server middleware to handle compressed requests and responses
h = compresshttp.ServerMiddleware(h, nil)
srv := httptest.NewServer(h)
// add client middleware to handle compressed responses
client := srv.Client()
client.Transport = compresshttp.ClientAcceptAllMiddleware(client.Transport, nil) // use default config: can also pass nil for defaults
req, _ := http.NewRequest("GET", srv.URL, nil)
resp, _ := client.Do(req)
got, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Println(string(got))
}
const gettysburgSentence = "Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal."
Output: Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal.
Example (AcceptClientCompression) ¶
The server middleware automatically detects and decompresses compressed request bodies. You can compress the request body with ClientEncodeGZIPMiddleware or ClientEncodeZSTDMiddleware and then send it to a server with ServerMiddleware.
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
_ "embed"
"gitlab.com/efronlicht/compresshttp"
)
func main() {
// handler echoes back the Content-Encoding header and request body length
var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Compress-HTTP-Using", r.Header.Get("Content-Encoding"))
body, _ := io.ReadAll(r.Body)
w.Write([]byte(fmt.Sprintf("length:%d", len(body))))
})
// add server middleware to handle compressed requests
h = compresshttp.ServerMiddleware(h, nil)
srv := httptest.NewServer(h)
client := srv.Client()
client.Transport = compresshttp.ClientEncodeGZIPMiddleware(client.Transport, nil)
r, _ := http.NewRequest("POST", srv.URL, strings.NewReader(gettysburgSentence))
resp, _ := client.Do(r)
got, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Printf("server used encoding: %q, response body: %s\n", resp.Header.Get("X-Compress-HTTP-Using"), string(got))
}
const gettysburgSentence = "Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal."
Output:
Example (ClientAcceptGZIP) ¶
Accept compressed responses from a server that allows gzip compression.
package main
import (
"compress/gzip"
"fmt"
"io"
"net/http"
"net/http/httptest"
_ "embed"
"gitlab.com/efronlicht/compresshttp"
)
const gettysburgSentence = "Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal."
func main() {
var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Encoding", "gzip")
gw := gzip.NewWriter(w)
_, _ = gw.Write([]byte(gettysburgSentence))
gw.Flush()
gw.Close()
})
srv := httptest.NewServer(h)
c := srv.Client()
c.Transport = compresshttp.ClientAcceptAllMiddleware(c.Transport, nil)
req, _ := http.NewRequest("GET", srv.URL, nil)
resp, _ := c.Do(req)
got, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Println(string(got))
}
Output: Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal.
Example (ClientAcceptZSTD) ¶
Accept compressed responses from a server that allows zstd compression.
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
_ "embed"
"github.com/klauspost/compress/zstd"
"gitlab.com/efronlicht/compresshttp"
)
const gettysburgSentence = "Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal."
func main() {
var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Encoding", "zstd")
zw, _ := zstd.NewWriter(w)
_, _ = zw.Write([]byte(gettysburgSentence))
zw.Flush()
zw.Close()
})
srv := httptest.NewServer(h)
c := srv.Client()
c.Transport = compresshttp.ClientAcceptAllMiddleware(c.Transport, nil)
req, _ := http.NewRequest("GET", srv.URL, nil)
resp, _ := c.Do(req)
got, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Println(string(got))
}
Output: Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal.
Index ¶
- Constants
- func NeverSkip(_ *http.Request) bool
- func NewBrotliWriterPool(opts brotli.WriterOptions) *brotliWriterPool
- func NewFlateWriterPool(level int) *flateWriterPool
- func NewGZIPWriterPool(lvl int) *gzipWriterPool
- type BrotliWriterOpts
- type ClientAcceptOpts
- type Encoding
- type FlateWriterOpts
- type GZIPWriterOpts
- type Quality
- type ServerConfig
- type SnappyWriterOpts
- type UnwrappingHandler
- type UnwrappingRoundTripper
- func ClientAcceptAllMiddleware(rt http.RoundTripper, cfg *ClientAcceptOpts) UnwrappingRoundTripper
- func ClientEncodeBrotliMiddleware(transport http.RoundTripper, opts *BrotliWriterOpts) UnwrappingRoundTripper
- func ClientEncodeFlateMiddleware(transport http.RoundTripper, cfg *FlateWriterOpts) UnwrappingRoundTripper
- func ClientEncodeGZIPMiddleware(rt http.RoundTripper, cfg *GZIPWriterOpts) UnwrappingRoundTripper
- func ClientEncodeSnappyMiddleware(transport http.RoundTripper, cfg *SnappyWriterOpts) UnwrappingRoundTripper
- func ClientEncodeZSTDMiddleware(transport http.RoundTripper, cfg *ZstdWriteOpts) UnwrappingRoundTripper
- type ZstdWriteOpts
Examples ¶
- Package
- Package (AcceptClientCompression)
- Package (ClientAcceptGZIP)
- Package (ClientAcceptZSTD)
- ClientEncodeBrotliMiddleware
- ClientEncodeFlateMiddleware
- ClientEncodeGZIPMiddleware
- ClientEncodeSnappyMiddleware
- ClientEncodeZSTDMiddleware
- DefaultAcceptQuality
- DefaultBrotliWriterOpts
- DefaultFlateWriterOpts
- DefaultGZIPWriterOpts
- DefaultSnappyWriterOpts
- Quality (NumberedQualities)
- Quality (OnlySomeEncodings)
Constants ¶
const DefaultZSTDThreshold = 2 * 1024 * 1024
DefaultZSTDThreshold is the default threshold for ZSTD compression in ClientEncodeZSTDMiddleware.
Variables ¶
This section is empty.
Functions ¶
func NeverSkip ¶
NeverSkip is a Skip function that never skips compression: this is the default behavior.
func NewBrotliWriterPool ¶ added in v0.3.0
func NewBrotliWriterPool(opts brotli.WriterOptions) *brotliWriterPool
NewWriterPool creates a new pool for brotli writers with the given options. Generally speaking, don't use this unless you know what you're doing: the default behavior is fine for most use cases.
func NewFlateWriterPool ¶ added in v0.3.0
func NewFlateWriterPool(level int) *flateWriterPool
func NewGZIPWriterPool ¶ added in v0.4.0
func NewGZIPWriterPool(lvl int) *gzipWriterPool
NewGZWriterPool creates a new WriterPool at the given compression level. This is used for level-varying brotli and gzip writers.
Types ¶
type BrotliWriterOpts ¶ added in v0.4.0
type BrotliWriterOpts struct {
brotli.WriterOptions
CompressThreshold int64 // optional size threshold (in bytes) for compressing bodies. Bodies smaller than this will not be compressed. Default is 512b.
Pool *brotliWriterPool
Skip func(*http.Request) bool // optional function to skip compression based on request. Default is nil (never skip).
}
Options for Brotli writers used by ServerMiddleware and ClientEncodeBrotliMiddleware. Default settings are provided by DefaultBrotliWriterOptions. See brotli.WriterOptions for details on the fields.
func DefaultBrotliWriterOpts ¶ added in v0.4.0
func DefaultBrotliWriterOpts() *BrotliWriterOpts
DefaultBrotliWriterPool is the default pool for brotli writers used by ClientEncodeBrotliMiddleware and ServerMiddleware.
- Quality: brotli.DefaultCompression (6)
- CompressThreshold: 512 bytes
- Pool: DefaultBrotliWriterPool
Example ¶
package main
import (
"fmt"
_ "embed"
"gitlab.com/efronlicht/compresshttp"
)
func main() {
opts := compresshttp.DefaultBrotliWriterOpts()
fmt.Printf(`quality=%d lgwin=%d compressthreshold=%d`, opts.Quality, opts.LGWin, opts.CompressThreshold)
}
Output: quality=6 lgwin=0 compressthreshold=512
func DefaultServerBrotliOpts ¶ added in v0.4.0
func DefaultServerBrotliOpts() *BrotliWriterOpts
DefaultServerBrotliOpts returns a ServerBrotliCfg with default settings.
- Level: brotli.DefaultCompression (6)
- Pool: DefaultBrotliWriterPool. If you change Level, either set Pool to nil or create a new WriterPool with the same level.
func (*BrotliWriterOpts) Validate ¶ added in v0.4.0
func (brotlicfg *BrotliWriterOpts) Validate() error
Validate the BrotliWriterOptions, sanity-checking its fields. This is called automatically by ServerMiddleware and ClientEncodeBrotliMiddleware, but can be called manually to verify options ahead of time.
type ClientAcceptOpts ¶ added in v0.4.0
type ClientAcceptOpts struct {
// Quality values for each supported encoding. If nil, DefaultAcceptQuality() is used.
// Quality values are not well-supported in practice.
Quality *Quality
}
Configures the ClientAcceptAllMiddleware behavior for GZIP decoding.
DefaultClientAcceptOpts() is usually sufficient.
func DefaultClientAcceptConfig ¶ added in v0.3.0
func DefaultClientAcceptConfig() *ClientAcceptOpts
DefaultClientAcceptConfig returns a ClientAcceptConfig with default settings:
- Quality: DefaultAcceptQuality() (all ones)
type Encoding ¶ added in v0.3.0
type Encoding int
Encoding is an enum representing and ordering supported compression encodings. In the case of ties, higher-valued encodings are preferred. (i.e, ZSTD > BROTLI > FLATE > GZIP > SNAPPY > NONE)
const ( NONE Encoding = iota SNAPPY Encoding = 1 GZIP Encoding = 2 FLATE Encoding = 3 BROTLI Encoding = 4 ZSTD Encoding = 5 N_ENCODING = 6 // proxy for number of encodings )
our priorities for encodings in case of ties. we respect user preference via quality settings, but if they don't, we choose (highest first.)
type FlateWriterOpts ¶ added in v0.4.0
type FlateWriterOpts struct {
Level int // Compression level: -2 to 9. Generally speaking, 0 is a mistake.
CompressThreshold int64 // optional size threshold (in bytes) for compressing bodies. Content-Length >= 0 and smaller than this will not be compressed. Default is 512b.
Pool *flateWriterPool // optional pool for flate writers. Must match Level.
Skip func(*http.Request) bool // optional function to skip compression based on request. Default is nil (never skip).
}
Options for ServerMiddleware and ClientEncodeFlateMiddleware. Default settings are provided by DefaultFlateWriterOptions.
func DefaultFlateWriterOpts ¶ added in v0.4.0
func DefaultFlateWriterOpts() *FlateWriterOpts
FlateWriterOption with default settings.
- Level: flate.DefaultCompression (-1)
- Pool: DefaultFlatePool. If you change the level, either set Pool to nil or create a new WriterPool with the same level.
- CompressThreshold: 512 bytes
Example ¶
package main
import (
"fmt"
_ "embed"
"gitlab.com/efronlicht/compresshttp"
)
func main() {
opts := compresshttp.DefaultFlateWriterOpts()
fmt.Printf(`level=%d compressthreshold=%d`, opts.Level, opts.CompressThreshold)
}
Output: level=-1 compressthreshold=512
func (*FlateWriterOpts) Validate ¶ added in v0.4.0
func (fcfg *FlateWriterOpts) Validate() error
Validate the FlateWriterOptions, sanity-checking its fields. This is called automatically by ServerMiddleware, but can be called manually to verify options ahead of time.
type GZIPWriterOpts ¶ added in v0.4.0
type GZIPWriterOpts struct {
Lvl int // gzip compression level
// optional pool for (*gzip.Writer). Must share the same compression level as Lvl. Don't touch this unless you really know what you're doing.
Pool *gzipWriterPool
CompressThreshold int64 // optional size threshold (in bytes) for compressing bodies. Content-Length >= 0 and smaller than this will not be compressed. Default is 512b.
Skip func(*http.Request) bool // optional function to skip compression for certain requests. Default is nil (never skip).
}
GZIPWriterOpts holds GZIP and PGZIP encoder and decoder options for the server middleware. See DefaultServerGZIPConfig for default settings.
func DefaultGZIPWriterOpts ¶ added in v0.4.0
func DefaultGZIPWriterOpts() *GZIPWriterOpts
DefaultGZIPWriterOpts returns a GZIPWriterOpts with default settings.
- Lvl: gzip.DefaultCompression (-1)
- Pool: DefaultGZIPWriterPool
- CompressThreshold: 512 bytes
- Skip: NeverSkip
Example ¶
package main
import (
"fmt"
_ "embed"
"gitlab.com/efronlicht/compresshttp"
)
func main() {
opts := compresshttp.DefaultGZIPWriterOpts()
fmt.Printf(`level=%d compressthreshold=%d`, opts.Lvl, opts.CompressThreshold)
}
Output: level=-1 compressthreshold=512
func (*GZIPWriterOpts) Validate ¶ added in v0.4.0
func (cfg *GZIPWriterOpts) Validate() error
Validate the ClientGZipEncodeConfig, sanity-checking its fields. This is called automatically by ClientEncodeGZIPMiddleware, but can be called manually to verify options ahead of time.
type Quality ¶ added in v0.4.0
type Quality struct {
GZIP, Flate, ZSTD, Brotli, Snappy float64 // quality values for each encoding; must be between 0 and 1 inclusive.
}
Quality values for each supported encoding: this is the 'order of preference' sent in the Accept-Encoding header. Quality values are not well-supported in practice: while this library properly generates and parses quality values, many servers and clients ignore the quality values and may fail to compress or decompress as expected - or worse! The headers are sent in descending order of quality to try and maximize compatibility, but there's no guarantee that the server will respect them. Quality values that aren't 0 or 1 should be used with caution. 0 will omit the encoding from the Accept-Encoding header, 1 is the default quality and will not add a quality tag.
Example (NumberedQualities) ¶
package main
import (
"fmt"
"gitlab.com/efronlicht/compresshttp"
)
func main() {
var q compresshttp.Quality
q.GZIP = 1.0
q.Brotli = 0.8
q.ZSTD = 0.5
for _, h := range q.AppendHeader(nil) {
fmt.Println("Accept-Encoding:", h)
}
}
Output: Accept-Encoding: gzip Accept-Encoding: br;q=0.8 Accept-Encoding: zstd;q=0.5
Example (OnlySomeEncodings) ¶
Create a client that accepts gzip and zstd compressed responses, ignoring brotli, snappy, and deflate.
package main
import (
"fmt"
"gitlab.com/efronlicht/compresshttp"
)
func main() {
var q compresshttp.Quality
q.GZIP, q.ZSTD = 1, 1
for _, h := range q.AppendHeader(nil) {
fmt.Println("Accept-Encoding:", h)
}
}
Output: Accept-Encoding: gzip Accept-Encoding: zstd
func DefaultAcceptQuality ¶ added in v0.3.0
func DefaultAcceptQuality() *Quality
DefaultAcceptQuality returns an AcceptQuality with all quality values set to 1.
Example ¶
package main
import (
"fmt"
_ "embed"
"gitlab.com/efronlicht/compresshttp"
)
func main() {
fmt.Printf("%+v\n", compresshttp.DefaultAcceptQuality())
}
Output: &{GZIP:1 Flate:1 ZSTD:1 Brotli:1 Snappy:1}
func (Quality) AppendHeader ¶ added in v0.4.0
AppendHeader appends Accept-Encoding header values for all encodings with non-zero quality to the provided slice of strings and returns the result. The headers are placed in descending order of quality, with equal-quality encodings in alphabetical order. This does NOT check that the quality values are valid: call Validate() first.
type ServerConfig ¶ added in v0.3.0
type ServerConfig struct {
ZSTDWrite []zstd.EOption // configures zstd encoding
GZIP *GZIPWriterOpts // configures gzip, pgzip, flate
Brotli *BrotliWriterOpts
FLATE *FlateWriterOpts
}
Configuration for ServerMiddleware. All fields are optional; if nil, defaults are used.
func (*ServerConfig) ValidatedWithDefaults ¶ added in v0.3.0
func (s *ServerConfig) ValidatedWithDefaults() (*ServerConfig, error)
ValidatedWithDefaults checks the ServerConfig for validity, filling in any nil sub-configs with defaults. This modifies the receiver. This is automatically called by ServerMiddleware.
type SnappyWriterOpts ¶ added in v0.4.0
type SnappyWriterOpts struct {
CompressThreshold int64 // size threshold to skip encoding if ContentLength is known and less than this value (i.e, 0 <= ContentLength < CompressThreshold). Default is 512b.
Skip func(r *http.Request) bool // optional function to skip compression for certain requests. If nil (default), never skip.
}
Configuration for ClientEncodeSnappyMiddleware and ServerMiddleware.
func DefaultSnappyWriterOpts ¶ added in v0.4.0
func DefaultSnappyWriterOpts() *SnappyWriterOpts
DefaultSnappyWriterOpts returns a SnappyWriterConfig with default settings.
- CompressThreshold: 512 bytes
Example ¶
package main
import (
"fmt"
_ "embed"
"gitlab.com/efronlicht/compresshttp"
)
func main() {
fmt.Printf("%+v\n", compresshttp.DefaultSnappyWriterOpts())
}
Output: &{CompressThreshold:512 Skip:<nil>}
type UnwrappingHandler ¶ added in v0.3.0
UnwrappingHandler is an http.Handler that exposes its inner handler for later use or removal.
func ServerMiddleware ¶
func ServerMiddleware(h http.Handler, cfg *ServerConfig) UnwrappingHandler
ServerMiddleware transparently compresses and decompresses HTTP request and response bodies encoded with GZIP, ZSTD, or Deflate. All configuration is optional, both overall and per subconfig; if nil, defaults are used.
Basic Usage ¶
var echo http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.Copy(w, r.Body)
})
h = compresshttp.ServerMiddleware(h, nil) // use default config
// h will now automatically decompress request bodies and compress response bodies as needed.
Request Decompression ¶
ServerMiddleware looks at the 'Content-Encoding' header of incoming requests and repeatedly decodes the body according to the encodings specified there. That is, it will remove any number of 'flate', 'gzip', or 'zstd' layers from the request body, stopping as soon as it finds an unrecognized encoding. It supports the following encodings:
- 'zstd', 'x-zstd', or 'zstandard': decompresses with zstd (taken from the stdlib/internal/zstd, vendored here)
- 'gzip' or 'x-gzip': decompresses with stdlib gzip or klauspost/pgzip, depending on the PGZipDecompress and PGDecompressThreshold settings.
- 'deflate', 'flat': decompresses with stdlib flate
- 'br' or 'brotli': decompresses with andybalholm/brotli
- 'snappy': decompresses with golang/snappy
any other 'Content-Encoding' value is ignored and the request body is passed through unmodified.
Response Compression ¶
ServerMiddleware looks at the 'Accept-Encoding' header of incoming requests to determine how to encode responses. It respects the quality values specified by the client, and picks the encoding with the highest quality value that it supports. In the case of unspecified or tied quality values, it prefers encodings in this order (highest first):
- zstd > brotli > deflate > gzip > snappy > none
See https://developer.mozilla.org/en-US/docs/Glossary/Quality_values for details on quality values.
If none of these encodings are accepted by the client, this passes the request to the underlying handler without modification.
type UnwrappingRoundTripper ¶ added in v0.3.0
type UnwrappingRoundTripper interface {
http.RoundTripper // see http.RoundTripper
Unwrap() http.RoundTripper // Unwrap returns the inner RoundTripper, if any.
}
UnwrappingRoundTripper is a http.RoundTripper that exposes its inner RoundTripper for later use or removal.
func ClientAcceptAllMiddleware ¶ added in v0.3.0
func ClientAcceptAllMiddleware(rt http.RoundTripper, cfg *ClientAcceptOpts) UnwrappingRoundTripper
ClientAcceptAllMiddleware is an HTTP RoundTripper that adds Accept-Encoding headers. It transparently decompresses response bodies encoded with GZIP, ZSTD, or Deflate. Wrap your existing RoundTripper with this to get automatic decompression support. If rt is nil, http.DefaultTransport is used.
Performance: ¶
All decoders use pooled readers for efficiency: this assumes that if you get one request with a given encoding, you'll likely get more soon after. This uses significantly less memory under steady-state load but can be slower or inefficient for one-offs or bursty loads.
func ClientEncodeBrotliMiddleware ¶ added in v0.3.0
func ClientEncodeBrotliMiddleware(transport http.RoundTripper, opts *BrotliWriterOpts) UnwrappingRoundTripper
ClientEncodeBrotliMiddleware is an HTTP RoundTripper that compresses request bodies with Brotli. You can pass nil for any argument to use default settings (transport: http.DefaultTransport, opts: DefaultBrotliWriterOptions).
Performance ¶
This spawns a new goroutine per request that does compression as the request body is read. This is necessary to avoid buffering the entire compressed body in memory before sending it to the server. For small request bodies, this may be less efficient than buffering the entire compressed body in memory first, but avoids indefinitely blocking on requests that lie about their Content-Length. This uses a pool of brotli.Writers for efficiency.
Pools must match the BrotliWriterOptions used to create them; using mismatched options will panic before returning the RoundTripper.
Example ¶
Encode request bodies with Brotli compression.
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
_ "embed"
"github.com/andybalholm/brotli"
"gitlab.com/efronlicht/compresshttp"
)
//go:embed warandpeace.txt
var warandpeace string
const gettysburgSentence = "Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal."
func main() {
const format = "%s: encoding: %q, content-length: %d\n"
var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, serverReq *http.Request) {
encoded, _ := io.ReadAll(serverReq.Body)
if serverReq.Header.Get("Content-Encoding") == "br" {
decoded := brotli.NewReader(strings.NewReader(string(encoded)))
got, _ := io.ReadAll(decoded)
if string(got) != gettysburgSentence && string(got) != warandpeace {
panic("decoded body mismatch")
}
}
fmt.Printf(format, "server", serverReq.Header.Get("Content-Encoding"), len(encoded))
w.WriteHeader(200)
})
srv := httptest.NewServer(h)
c := srv.Client()
c.Transport = compresshttp.ClientEncodeBrotliMiddleware(c.Transport, nil)
sendReq := func(s string) {
clientReq, _ := http.NewRequest("POST", srv.URL, strings.NewReader(s))
fmt.Printf(format, "client", clientReq.Header.Get("Content-Encoding"), clientReq.ContentLength)
_, _ = c.Do(clientReq)
}
sendReq(gettysburgSentence)
sendReq(warandpeace)
}
Output: client: encoding: "", content-length: 176 server: encoding: "", content-length: 176 client: encoding: "", content-length: 3288446 server: encoding: "br", content-length: 1009174
func ClientEncodeFlateMiddleware ¶ added in v0.3.0
func ClientEncodeFlateMiddleware(transport http.RoundTripper, cfg *FlateWriterOpts) UnwrappingRoundTripper
Example ¶
Encode request bodies with Flate (deflate) compression.
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
_ "embed"
"gitlab.com/efronlicht/compresshttp"
)
//go:embed warandpeace.txt
var warandpeace string
const gettysburgSentence = "Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal."
func main() {
const format = "%s: encoding: %q, content-length: %d\n"
var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, serverReq *http.Request) {
body := io.Reader(serverReq.Body)
got, _ := io.ReadAll(body)
fmt.Printf(format, "server", serverReq.Header.Get("Content-Encoding"), len(got))
w.WriteHeader(200)
})
srv := httptest.NewServer(h)
c := srv.Client()
c.Transport = compresshttp.ClientEncodeFlateMiddleware(c.Transport, nil)
sendReq := func(s string) {
clientReq, _ := http.NewRequest("POST", srv.URL, strings.NewReader(s))
fmt.Printf(format, "client", clientReq.Header.Get("Content-Encoding"), clientReq.ContentLength)
_, _ = c.Do(clientReq)
}
sendReq(gettysburgSentence) // small bodies are not compressed by default to save on overhead
sendReq(warandpeace) // but large bodies are.
}
Output: client: encoding: "", content-length: 176 server: encoding: "", content-length: 176 client: encoding: "", content-length: 3288446 server: encoding: "deflate", content-length: 1169922
func ClientEncodeGZIPMiddleware ¶
func ClientEncodeGZIPMiddleware(rt http.RoundTripper, cfg *GZIPWriterOpts) UnwrappingRoundTripper
ClientEncodeGZIPMiddleware is an HTTP RoundTripper that compresses request bodies or GZIP or PGZIP if they're large enough.
GZIP is widely-supported but somewhat slow and inefficient for large bodies.
Performance ¶
This spawns a new goroutine per request that does compression as the request body is read. This is necessary to avoid buffering the entire compressed body in memory before sending it to the server. For small request bodies, this may be less efficient than buffering the entire compressed body in memory first, but avoids indefinitely blocking on requests that lie about their Content-Length. You can adjust the CompressThreshold field in ClientGZipConfig to control when compression is applied based on the request's Content-Length. In the default (auto), mode, pgzip is used for large bodies (>=2MiB), the stdlib for medium bodies (>=2KiB and <2MiB), and no compression for small bodies (<2KiB). See ClientGZipConfig for details.
Warnings ¶
- not all servers support compressed request bodies: make sure the server you're talking to can handle this.
- level 0 (gzip.NoCompression) has almost no use case: it adds gzip headers but does no compression: this function will warn you about this unless you set the COMPRESSHTTP_WARN_GZIP_NO_COMPRESSION=0 environment variable.
This function panics if the provided configuration is invalid. Call cfg.Validate() to check for errors ahead of time.v
Example ¶
Encode request bodies with GZIP compression. Compression will be skipped for small bodies, done with stdlib gzip for medium bodies, and with pgzip for large bodies.
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"strings"
_ "embed"
"gitlab.com/efronlicht/compresshttp"
)
//go:embed warandpeace.txt
var warandpeace string
const gettysburgSentence = "Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal."
func main() {
const format = "%s: encoding: %q, content-length: %v\n"
var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, serverReq *http.Request) {
got, _ := io.ReadAll(serverReq.Body)
fmt.Fprintf(os.Stdout, format, "server", serverReq.Header.Get("Content-Encoding"), len(got))
w.WriteHeader(200)
})
srv := httptest.NewServer(h)
c := srv.Client()
c.Transport = compresshttp.ClientEncodeGZIPMiddleware(c.Transport, nil)
sendReq := func(s string) {
clientReq, _ := http.NewRequest("POST", srv.URL, strings.NewReader(s)) // create request
fmt.Printf(format, "client", clientReq.Header.Get("Content-Encoding"), clientReq.ContentLength)
_, _ = c.Do(clientReq) // send request
}
// small bodies are not compressed by default to save on overhead:
sendReq(gettysburgSentence)
// but larger bodies are...
sendReq(warandpeace)
}
Output: client: encoding: "", content-length: 176 server: encoding: "", content-length: 176 client: encoding: "", content-length: 3288446 server: encoding: "gzip", content-length: 1169940
func ClientEncodeSnappyMiddleware ¶ added in v0.3.0
func ClientEncodeSnappyMiddleware(transport http.RoundTripper, cfg *SnappyWriterOpts) UnwrappingRoundTripper
ClientEncodeSnappyMiddleware is an HTTP RoundTripper that compresses request bodies with Snappy.
Snappy is a fast but not very efficient compression algorithm; it's largely fallen out of favor as ZSTD and Brotli have become more widespread and CPUs have become faster.
Performance ¶
This uses a buffered writer and spawns a goroutine to do compression as the request body is read: for small bodies, it may be more efficient to do in-memory compression first or skip compression entirely. Use the CompressThreshold field in SnappyWriterConfig to control this behavior.
Writers are pooled for efficiency.
Example ¶
Encode request bodies with Snappy compression.
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
_ "embed"
"gitlab.com/efronlicht/compresshttp"
)
//go:embed warandpeace.txt
var warandpeace string
const gettysburgSentence = "Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal."
func main() {
const format = "%s: encoding: %q, content-length: %d\n"
var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, serverReq *http.Request) {
got, _ := io.ReadAll(serverReq.Body)
fmt.Printf(format, "server", serverReq.Header.Get("Content-Encoding"), len(got))
})
srv := httptest.NewServer(h)
c := srv.Client()
c.Transport = compresshttp.ClientEncodeSnappyMiddleware(c.Transport, nil)
sendReq := func(s string) {
clientReq, _ := http.NewRequest("POST", srv.URL, strings.NewReader(s))
fmt.Printf(format, "client", clientReq.Header.Get("Content-Encoding"), clientReq.ContentLength)
_, _ = c.Do(clientReq)
}
sendReq(gettysburgSentence) // small bodies are not compressed by default
sendReq(warandpeace) // but large ones are.
}
Output: client: encoding: "", content-length: 176 server: encoding: "", content-length: 176 client: encoding: "", content-length: 3288446 server: encoding: "snappy", content-length: 1913059
func ClientEncodeZSTDMiddleware ¶
func ClientEncodeZSTDMiddleware(transport http.RoundTripper, cfg *ZstdWriteOpts) UnwrappingRoundTripper
ClientEncodeZSTDMiddleware is an HTTP RoundTripper that compresses request bodies with ZSTD if they're large enough. You can pass nil for any argument to use default settings (transport: http.DefaultTransport, cfg: DefaultClientZSTDEncodeConfig()).
Performance ¶
- This spawns a new goroutine per request that does compression as the request body is read. This is necessary to avoid buffering the entire compressed body in memory before sending it to the server.
- For small request bodies, this may be less efficient than buffering the entire compressed body in memory first, but avoids indefinitely blocking on requests that lie about their Content-Length.
- This uses a new zstd.Encoder per request: zstd.Encoder.Reset() has some issues that make pooling difficult. TODO: revisit this in the future.
Warning ¶
ZSTD compression is not transparently supported by most servers: don't expect this to work unless someone has a mirroring middleware on the server side.
Bounds: ¶
This function panics if the provided configuration is invalid. Invalid configuration includes:
- negative CompressThreshold
- invalid EncoderOptions
Caveats/Improvements: ¶
- we create new zstd.Writers for every request, which leads to memory presure via garbage collection. ideally, we would reuse these via pooling, but klauspost/compress/zstd's Reset() method has some issues that make pooling difficult.
- TODO: when the stdlib supports zstd WRITING natively, switch to that and pool those writers instead.
Example ¶
As a client, encode request bodies with ZSTD compression.
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
_ "embed"
"gitlab.com/efronlicht/compresshttp"
)
//go:embed warandpeace.txt
var warandpeace string
const gettysburgSentence = "Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal."
func main() {
const format = "%s: encoding: %q, content-length: %d\n"
var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, serverReq *http.Request) {
got, _ := io.ReadAll(serverReq.Body)
fmt.Printf(format, "server", serverReq.Header.Get("Content-Encoding"), len(got))
w.WriteHeader(200)
})
srv := httptest.NewServer(h)
c := srv.Client()
c.Transport = compresshttp.ClientEncodeZSTDMiddleware(c.Transport, nil)
sendReq := func(s string) {
clientReq, _ := http.NewRequest("POST", srv.URL, strings.NewReader(s))
fmt.Printf(format, "client", clientReq.Header.Get("Content-Encoding"), clientReq.ContentLength)
_, _ = c.Do(clientReq)
}
// small bodies are not compressed by default...
sendReq(gettysburgSentence)
// ... but large ones are:
sendReq(warandpeace)
}
Output: client: encoding: "", content-length: 176 server: encoding: "", content-length: 176 client: encoding: "", content-length: 3288446 server: encoding: "zstd", content-length: 1137550
type ZstdWriteOpts ¶ added in v0.4.0
type ZstdWriteOpts struct {
CompressThreshold int64 // size threshold to skip encoding if ContentLength is known and less than this value (i.e, 0 <= ContentLength < CompressThreshold). Default is 2KiB.
EncoderOptions []zstd.EOption // options to pass to zstd.NewWriter. Default is nil.
Skip func(r *http.Request) bool // optional function to skip compression for certain requests. If nil (default), never skip.
}
ZstdWriteOpts configures ZSTD encoding for ClientEncodeZSTDMiddleware and ServerMiddleware.
func DefaultClientZSTDEncodeConfig ¶
func DefaultClientZSTDEncodeConfig() *ZstdWriteOpts
DefaultClientZSTDEncodeConfig returns a ZSTDEncodeConfig with default settings:
- CompressThreshold: 2KiB,
- EncoderOptions: nil
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
stdlibzstd
Package zstd provides a decompressor for zstd streams, described in RFC 8878.
|
Package zstd provides a decompressor for zstd streams, described in RFC 8878. |
|
package prototype contains prototype implementations of various compresshttp middlewares.
|
package prototype contains prototype implementations of various compresshttp middlewares. |
|
alpha
Package alpha contains alpha and/or 'application'-quality prototype implementations of various compresshttp middlewares.
|
Package alpha contains alpha and/or 'application'-quality prototype implementations of various compresshttp middlewares. |
|
beta
Package beta contains a prototype HTTP client middleware implementation that handles compresshttp compression and decompression for gzip, deflate (flate), and zstd.
|
Package beta contains a prototype HTTP client middleware implementation that handles compresshttp compression and decompression for gzip, deflate (flate), and zstd. |