Compression

app.Use(muzak.Compress(muzak.CompressionOptions{}))

That is the whole installation. The middleware negotiates an encoding from Accept-Encoding, compresses the bodies worth compressing, and records Vary on every response either way.

Install it with App.Use, which puts it inside the built-in chain, so it already has a request identifier and is already covered by panic recovery.

What is negotiated

gzip is preferred over deflate, and an explicit refusal such as gzip;q=0 is honoured. A client that asks for neither gets an uncompressed body.

Vary: Accept-Encoding is set on every response whether or not it was compressed, so a cache cannot hand a compressed body to a client that cannot read it.

What is left alone

A response that......is not compressed
is smaller than MinSizea body that already fits in one packet cannot arrive sooner by shrinking
carries a media type that is not text-likean image, a video or an archive is already compressed
the handler encoded itselfa Content-Encoding is already set
carries no bodythere is nothing to compress
is a rangea compressed range is not the range that was asked for
is an event streamholding events in a compressor's window until something forces them out is the one thing a stream cannot survive

muzak.DefaultCompressionMinSize is 1400 bytes, a little under one ethernet MTU. A response that already fits in a single packet cannot be made to arrive sooner by shrinking it, and compressing it spends CPU on both ends to save nothing.

Options

app.Use(muzak.Compress(muzak.CompressionOptions{
    Level:        muzak.CompressionBest,
    MinSize:      2048,
    ContentTypes: []string{"text/", "application/json", "+json", "image/svg+xml"},
}))
FieldDefaultEffect
LevelCompressionDefaultHow hard the compressor works
MinSizeDefaultCompressionMinSizeThe smallest body worth compressing. A response whose length is not known in advance is buffered up to this size before the decision is made
ContentTypesa built-in list of text-like typesWhich media types are compressed
LevelFor
CompressionDefaultBalances speed against size, which is the right choice until a measurement says otherwise
CompressionFastestA service that is CPU bound, or serving large bodies to a fast network
CompressionBestA service whose clients are on slow or metered connections

There are three levels rather than an integer because the underlying range has invalid values in it and there is nothing useful to do with a level of 42 at run time.

An entry in ContentTypes is matched as a prefix, so text/ covers every text type, and an entry beginning with + matches a structured syntax suffix, so +json covers application/problem+json.

What it is worth

On the framework's own example application, compression takes 91% off the OpenAPI document and 68% off the documentation page. A JSON listing of any size behaves much the same. A small response, an image and an event stream are all untouched, which is the point of the exclusions above.

Compression and secrecy

Compression and secrecy interact badly, and it is worth knowing where.

When a response mixes a secret with something the client controls, its compressed length leaks how much the two have in common. That is what the BREACH attack recovers a token from, over many requests. Muzak's own responses do not mix the two, but a handler that reflects a query parameter back alongside a CSRF token does.

Where that is possible, either leave compression off for the route or stop reflecting the input.

Ordering with your own middleware

Middleware installed first is outermost, so a middleware that reports a duration sees the time spent compressing only if it is installed before the compressor.

// ProcessTime is outermost of the two, so the duration it reports includes the
// time spent compressing.
app.Use(core.ProcessTime())
app.Use(muzak.Compress(muzak.CompressionOptions{}))

Anything that wraps the response writer needs to implement Unwrap() http.ResponseWriter, or it will break flushing and hijacking for everything inside it. See Middleware.

Checking it

curl -s -H 'Accept-Encoding: gzip' -o /dev/null -D - http://localhost:8080/openapi.json
HTTP/1.1 200 OK
Content-Type: application/json
Content-Encoding: gzip
Vary: Accept-Encoding
func TestOpenAPIIsCompressed(t *testing.T) {
    client := testclient.New(t, buildApp())

    res := client.Get("/openapi.json", testclient.Header("Accept-Encoding", "gzip"))

    res.AssertStatus(http.StatusOK)
    res.AssertHeader("Vary", "Accept-Encoding")
}

Where to go next

Static Files and Frontends covers the assets this makes smaller, and Server-Sent Events covers the one response kind compression skips on purpose.

Open source under MIT / Apache-2.0 · sustained by the people who ship on it.Built on net/http, and nothing else.