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,
  "org.typelevel" %% "log4cats-slf4j" % log4catsVersion,
)

And we need some imports.

import cats.effect._
import org.http4s._
import org.http4s.dsl.io._
import org.http4s.implicits._
import org.typelevel.log4cats.LoggerFactory
import org.typelevel.log4cats.slf4j.Slf4jFactory

If you're in a REPL, we also need a runtime and a logger factory:

import cats.effect.unsafe.IORuntime
implicit val runtime: IORuntime = cats.effect.unsafe.IORuntime.global
implicit val loggerFactory: LoggerFactory[IO] = Slf4jFactory.create[IO]

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

val request = Request[IO](Method.GET, uri"/")
// Do not call 'unsafeRun' in your code - see note at bottom.
val response = service.orNotFound(request).unsafeRunSync()
// response: Response[[A]IO[A]] = (
//    = Status(code = 200),
//    = HttpVersion(major = 1, minor = 1),
//    = Headers(Content-Type: text/plain; charset=UTF-8, Content-Length: 117),
//    = Strict(
//     bytes = Chunk(
//       bytes = View(
//         at = scodec.bits.ByteVector$AtArray@6e620934,
//         offset = 0L,
//         size = 117L
//       )
//     )
//   ),
//    = org.typelevel.vault.Vault@199e3801
// )
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)
// Do not call 'unsafeRun' in your code - see note at bottom.
val respNormal = serviceZip.orNotFound(request).unsafeRunSync()
// respNormal: Response[[A]IO[A]] = (
//    = Status(code = 200),
//    = HttpVersion(major = 1, minor = 1),
//    = Headers(Content-Type: text/plain; charset=UTF-8, Content-Length: 117),
//    = Strict(
//     bytes = Chunk(
//       bytes = View(
//         at = scodec.bits.ByteVector$AtArray@1dd0453e,
//         offset = 0L,
//         size = 117L
//       )
//     )
//   ),
//    = org.typelevel.vault.Vault@306f7b88
// )
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@163b68e1
// )

// Do not call 'unsafeRun' in your code - see note at bottom.
val respZip = serviceZip.orNotFound(requestZip).unsafeRunSync()
// respZip: Response[[A]IO[A]] = (
//    = Status(code = 200),
//    = HttpVersion(major = 1, minor = 1),
//    = Headers(Content-Type: text/plain; charset=UTF-8, Content-Encoding: gzip),
//    = Streamed(body = Stream(..), length = None),
//    = org.typelevel.vault.Vault@7d11382f
// )
val bodyZip = respZip.as[String].unsafeRunSync()
// bodyZip: String = "\u001f�\b\u0000\u0000\u0000\u0000\u0000\u0000\u0003�T(J-HM,Qȭ,N�IS(�H�S�T�U(�KI-R(.)J-.�S�2\u0000UX�<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].