compresshttp

package module
v0.4.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 1, 2025 License: BSD-3-Clause-Clear Imports: 21 Imported by: 0

README

compresshttp

compresshttp provides HTTP client and server middleware for transparent compression and decompression of HTTP request and response bodies compressed using gzip, zstd, or deflate using the 'Accept-Encoding' and 'Content-Encoding' headers.

The package documentation is the source of truth for documentation.

Installation (as a library)

#!/usr/bin/env bash
# Navigate to the folder containing your `go.mod` file:
cd $HOME/go/src/path/to/my/project

# install using go get
go get gitlab.com/efronlicht/compresshttp@latest

See the package examples for details on usage.

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."
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

Examples

Constants

View Source
const DefaultZSTDThreshold = 2 * 1024 * 1024

DefaultZSTDThreshold is the default threshold for ZSTD compression in ClientEncodeZSTDMiddleware.

Variables

This section is empty.

Functions

func NeverSkip

func NeverSkip(_ *http.Request) bool

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.)

func (Encoding) String added in v0.3.0

func (e Encoding) String() string

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

func (aq Quality) AppendHeader(h []string) []string

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.

func (Quality) Validate added in v0.4.0

func (aq Quality) Validate() error

Validate checks that all quality values are between 0 and 1 inclusive.

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

type UnwrappingHandler interface {
	http.Handler
	Unwrap() http.Handler
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL