Skip to main content

Coding Directives

Coding directives compress responses and decompress requests with gzip and deflate through fs2's Compression, on every platform.

encodeResponse​

Encodes the response with the coding the client prefers in Accept-Encoding, choosing between identity, gzip and deflate (in that order on ties). Without the header, or when the client lists neither gzip nor deflate, the response is left alone; it is rejected with an UnacceptedResponseEncodingRejection (406 when sealed) only when the client explicitly refuses identity as well. Responses that already carry Content-Encoding, have no entity, or have a non-compressible media type are never touched. Content-Length is removed from an encoded response.

Each chunk of the response is compressed and flushed as it is produced, as Pekko's encoders do, so a streamed response (server-sent events, a slow producer) reaches the client without waiting for the compressor to fill its buffer. Every flush costs a few bytes and resets the compressor's look-ahead, so a stream of small chunks compresses much worse: one chunk per line of JSON (about 50 bytes) came out nearly three times larger than one chunk, 16-byte chunks six times. Regroup small chunks before completing when size matters more than latency (chunkMin, or groupWithin to bound the delay). On Scala Native the flush is not applied (scala-native's Deflater ignores the flush mode), so the output is buffered as before.

val route: Route = encodeResponse {
complete("a large text body")
}

encodeResponseWith​

Like encodeResponse but with an explicit list of codings. Without Accept-Encoding the first one is used; when the client accepts none of them the request is rejected with an UnacceptedResponseEncodingRejection (406 when sealed).

import org.http4s.ContentCoding

val route: Route = encodeResponseWith(ContentCoding.gzip) {
complete("always gzipped unless refused")
}

responseEncodingAccepted​

Passes when the client lists the given coding (or *) with a positive q-value in Accept-Encoding, or sends no Accept-Encoding at all; rejects with an UnacceptedResponseEncodingRejection otherwise.

decodeRequest​

Decompresses a gzip or deflate encoded request body before the inner route reads it; unencoded requests and requests known to carry no body (Content-Length: 0, or HTTP/1.x with neither a length nor chunking) pass through. Any other Content-Encoding is rejected with UnsupportedRequestEncodingRejections (400 when sealed). A corrupt compressed body fails the entity stream with a 400 Bad Request ("The request's encoding is corrupt"), whoever reads it. The decoded request carries Transfer-Encoding: chunked in place of its Content-Length, so requestEntityPresent still sees a body.

The decoded body is bounded to CodingDirectives.DefaultDecodeMaxBytes (8 MiB, Pekko's decode-max-size), so a small compressed body cannot expand without limit; exceeding it fails with 413 Payload Too Large. A withSizeLimit or withoutSizeLimit inside decodeRequest replaces that bound for the decompressed bytes; outside it only bounds the compressed bytes.

val route: Route = decodeRequest {
entity(as[String]) { body => complete(s"${body.length} chars") }
}

decodeRequestWith​

decodeRequestWith(coding) accepts only requests encoded with that coding (or any request when the coding is identity). decodeRequestWith(codings: _*) tries each in turn.

requestEncodedWith​

Rejects with an UnsupportedRequestEncodingRejection unless the request's Content-Encoding (identity when absent) matches the given coding.

withPrecompressedMediaTypeSupport​

Adds Content-Encoding: gzip to a response that one of the static file directives served from a .svgz or name.ext.gz file (Pekko's Gzipped media types) when the response's content type is not an archive (application/gzip) and no Content-Encoding is set yet. http4s media types carry no "gzipped" flag, so the directive looks at the served file name and the final content type instead. The static file directives already add the header when their ContentTypeResolver typed the file, so the wrapper is only needed around a file served with an explicit content type. encodeResponse never compresses such a response, so the two directives can be nested in either order.

val route: Route = path("logo.svg") {
withPrecompressedMediaTypeSupport {
getFromFile("/var/www/static/logo.svgz", `Content-Type`(MediaType.image.`svg+xml`))
}
}