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 MinSize | a body that already fits in one packet cannot arrive sooner by shrinking |
| carries a media type that is not text-like | an image, a video or an archive is already compressed |
| the handler encoded itself | a Content-Encoding is already set |
| carries no body | there is nothing to compress |
| is a range | a compressed range is not the range that was asked for |
| is an event stream | holding 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"},
}))
| Field | Default | Effect |
|---|---|---|
Level | CompressionDefault | How hard the compressor works |
MinSize | DefaultCompressionMinSize | The smallest body worth compressing. A response whose length is not known in advance is buffered up to this size before the decision is made |
ContentTypes | a built-in list of text-like types | Which media types are compressed |
| Level | For |
|---|---|
CompressionDefault | Balances speed against size, which is the right choice until a measurement says otherwise |
CompressionFastest | A service that is CPU bound, or serving large bodies to a fast network |
CompressionBest | A 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.