GZip Compression
Http4s provides Middleware, named GZip
, for allowing for the compression of the Response
body using GZip.
Examples in this document have the following dependencies.
libraryDependencies ++= Seq(
"org.http4s" %% "http4s-dsl" % http4sVersion,
"org.http4s" %% "http4s-server" % http4sVersion
)
And we need some imports.
import cats.effect._
import org.http4s._
import org.http4s.dsl.io._
import org.http4s.implicits._
If you're in a REPL, we also need a runtime:
import cats.effect.unsafe.IORuntime
implicit val runtime: IORuntime = cats.effect.unsafe.IORuntime.global
Let's start by making a simple service that returns a (relatively) large string
in its body. We'll use as[String]
to examine the body.
val service = HttpRoutes.of[IO] {
case _ =>
Ok("I repeat myself when I'm under stress. " * 3)
}
// service: HttpRoutes[IO] = Kleisli(
// run = org.http4s.HttpRoutes$$$Lambda$21885/683693719@764648c5
// )
val request = Request[IO](Method.GET, uri"/")
// request: Request[IO] = (
// = GET,
// = Uri(scheme = None, authority = None, path = /, query = , fragment = None),
// = HttpVersion(major = 1, minor = 1),
// = Headers(),
// = Entity.Empty,
// = org.typelevel.vault.Vault@3a343304
// )
// Do not call 'unsafeRun' in your code - see note at bottom.
val response = service.orNotFound(request).unsafeRunSync()
// response: Response[IO[A]] = (
// = Status(code = 200),
// = HttpVersion(major = 1, minor = 1),
// = Headers(Content-Type: text/plain; charset=UTF-8, Content-Length: 117),
// = Strict(
// chunk = ArraySlice(
// values = Array(
// 73,
// 32,
// 114,
// 101,
// 112,
// 101,
// 97,
// 116,
// 32,
// 109,
// 121,
// 115,
// 101,
// 108,
// 102,
// 32,
// 119,
// 104,
// 101,
// 110,
// 32,
// 73,
// 39,
// 109,
// 32,
// 117,
// 110,
// 100,
// 101,
// 114,
// 32,
// 115,
// 116,
// 114,
// 101,
// 115,
// 115,
// 46,
// 32,
// 73,
// 32,
// 114,
// ...
val body = response.as[String].unsafeRunSync()
// body: String = "I repeat myself when I'm under stress. I repeat myself when I'm under stress. I repeat myself when I'm under stress. "
body.length
// res0: Int = 117
Now we can wrap the service in the GZip
middleware.
import org.http4s.server.middleware._
val serviceZip = GZip(service)
// serviceZip: Http[cats.data.OptionT[IO, β$0$], IO[A]] = Kleisli(
// run = org.http4s.server.middleware.GZip$$$Lambda$23606/1547026604@7b31bfd3
// )
// Do not call 'unsafeRun' in your code - see note at bottom.
val respNormal = serviceZip.orNotFound(request).unsafeRunSync()
// respNormal: Response[IO[A]] = (
// = Status(code = 200),
// = HttpVersion(major = 1, minor = 1),
// = Headers(Content-Type: text/plain; charset=UTF-8, Content-Length: 117),
// = Strict(
// chunk = ArraySlice(
// values = Array(
// 73,
// 32,
// 114,
// 101,
// 112,
// 101,
// 97,
// 116,
// 32,
// 109,
// 121,
// 115,
// 101,
// 108,
// 102,
// 32,
// 119,
// 104,
// 101,
// 110,
// 32,
// 73,
// 39,
// 109,
// 32,
// 117,
// 110,
// 100,
// 101,
// 114,
// 32,
// 115,
// 116,
// 114,
// 101,
// 115,
// 115,
// 46,
// 32,
// 73,
// 32,
// 114,
// ...
val bodyNormal = respNormal.as[String].unsafeRunSync()
// bodyNormal: String = "I repeat myself when I'm under stress. I repeat myself when I'm under stress. I repeat myself when I'm under stress. "
bodyNormal.length
// res1: Int = 117
So far, there was no change. That's because the caller needs to inform us that
they will accept GZipped responses via an Accept-Encoding
header. Acceptable
values for the Accept-Encoding
header are "gzip", "x-gzip", and *""**.
val requestZip = request.putHeaders("Accept-Encoding" -> "gzip")
// requestZip: request.SelfF[IO] = (
// = GET,
// = Uri(scheme = None, authority = None, path = /, query = , fragment = None),
// = HttpVersion(major = 1, minor = 1),
// = Headers(Accept-Encoding: gzip),
// = Entity.Empty,
// = org.typelevel.vault.Vault@3a343304
// )
// Do not call 'unsafeRun' in your code - see note at bottom.
val respZip = serviceZip.orNotFound(requestZip).unsafeRunSync()
// respZip: Response[IO[A]] = (
// = Status(code = 200),
// = HttpVersion(major = 1, minor = 1),
// = Headers(Content-Type: text/plain; charset=UTF-8, Content-Encoding: gzip),
// = Default(body = Stream(..), length = None),
// = org.typelevel.vault.Vault@6e245718
// )
val bodyZip = respZip.as[String].unsafeRunSync()
// bodyZip: String = "\u001f\ufffd\b\u0000\u0000\u0000\u0000\u0000\u0000\u0003\ufffdT(J-HM,Q\u022d,N\ufffdIS(\ufffdH\ufffdS\ufffdT\ufffdU(\ufffdKI-R(.)J-.\ufffdS\ufffd2\u0000UX\ufffd<u\u0000\u0000\u0000"
bodyZip.length
// res2: Int = 59
Notice how the response no longer looks very String-like and it's shorter in
length. Also, there is a Content-Encoding
header in the response with a value
of "gzip".
As described in Middleware, services and middleware can be composed such that only some of your endpoints are GZip enabled.
NOTE: In this documentation, we are calling unsafeRunSync
to extract values out
of a service or middleware code. You can work with values while keeping them inside the
F
using map
, flatMap
and/or for
. Remember, your service returns an
F[Response]
.