Middleware

A middleware is an abstraction around a service that provides a means of manipulating the Request sent to service, and/or the Response returned by the service. In some cases, such as Authentication, middleware may even prevent the service from being called.

At its most basic, middleware is a function that takes one service and returns another. The middleware function can take any additional parameters it needs to perform its task. Let's look at a simple example.

For this, we'll need a dependency on the http4s dsl.

libraryDependencies ++= Seq(
  "org.http4s" %% "http4s-dsl" % http4sVersion
)

and some imports.

import cats.data.Kleisli
import cats.effect._
import cats.syntax.all._
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

Then, we can create a middleware that adds a header to successful responses from the underlying HttpRoutes like this.

def myMiddle(service: HttpRoutes[IO], header: Header.ToRaw): HttpRoutes[IO] = Kleisli { (req: Request[IO]) =>
  service(req).map {
    case Status.Successful(resp) =>
      resp.putHeaders(header)
    case resp =>
      resp
  }
}

All we do here is pass the request to the service, which returns an F[Response]. So, we use map to get the request out of the task, add the header if the response is a success, and then pass the response on. We could just as easily modify the request before we passed it to the service.

Now, let's create a simple service. As mentioned between service and dsl, because HttpRoutes is implemented as a Kleisli, which is just a function at heart, we can test a service without a server. Due to an HttpRoutes[F] returns a F[Response[F]], we need to call unsafeRunSync on the result of the function to extract the Response[F]. Note that basically, you shouldn't use unsafeRunSync in your application. Here we use it for demo reasons only.

val service = HttpRoutes.of[IO] {
  case GET -> Root / "bad" =>
    BadRequest()
  case _ =>
    Ok()
}
// service: HttpRoutes[IO] = Kleisli(
//   run = org.http4s.HttpRoutes$$$Lambda$20111/1400366070@2781d2d3
// )

val goodRequest = Request[IO](Method.GET, uri"/")
// goodRequest: Request[IO] = (
//    = GET,
//    = Uri(scheme = None, authority = None, path = /, query = , fragment = None),
//    = HttpVersion(major = 1, minor = 1),
//    = Headers(),
//    = Entity.Empty,
//    = org.typelevel.vault.Vault@637ca333
// )
val badRequest = Request[IO](Method.GET, uri"/bad")
// badRequest: Request[IO] = (
//    = GET,
//    = Uri(
//     scheme = None,
//     authority = None,
//     path = /bad,
//     query = ,
//     fragment = None
//   ),
//    = HttpVersion(major = 1, minor = 1),
//    = Headers(),
//    = Entity.Empty,
//    = org.typelevel.vault.Vault@3449af44
// )

service.orNotFound(goodRequest).unsafeRunSync()
// res0: Response[IO[A]] = (
//    = Status(code = 200),
//    = HttpVersion(major = 1, minor = 1),
//    = Headers(Content-Length: 0),
//    = Entity.Empty,
//    = org.typelevel.vault.Vault@4049f645
// )
service.orNotFound(badRequest).unsafeRunSync()
// res1: Response[IO[A]] = (
//    = Status(code = 400),
//    = HttpVersion(major = 1, minor = 1),
//    = Headers(Content-Length: 0),
//    = Entity.Empty,
//    = org.typelevel.vault.Vault@6977a3d5
// )

Now, we'll apply the service to our middleware function to create a new service, and try it out.

val modifiedService = myMiddle(service, "SomeKey" -> "SomeValue");

modifiedService.orNotFound(goodRequest).unsafeRunSync()
modifiedService.orNotFound(badRequest).unsafeRunSync()

Note that the successful response has your header added to it.

If you intend to use you middleware in multiple places, you may want to implement it as an object and use the apply method.

object MyMiddle {
  def addHeader(resp: Response[IO], header: Header.ToRaw) =
    resp match {
      case Status.Successful(resp) => resp.putHeaders(header)
      case resp => resp
    }

  def apply(service: HttpRoutes[IO], header: Header.ToRaw) =
    service.map(addHeader(_, header))
}

val newService = MyMiddle(service, "SomeKey" -> "SomeValue")
// newService: Kleisli[cats.data.OptionT[IO, β$0$], Request[IO], Response[IO[A]]] = Kleisli(
//   run = scala.Function1$$Lambda$20191/84086833@6f14f0f3
// )

newService.orNotFound(goodRequest).unsafeRunSync()
// res4: Response[IO[A]] = (
//    = Status(code = 200),
//    = HttpVersion(major = 1, minor = 1),
//    = Headers(Content-Length: 0, SomeKey: SomeValue),
//    = Entity.Empty,
//    = org.typelevel.vault.Vault@165e41da
// )
newService.orNotFound(badRequest).unsafeRunSync()
// res5: Response[IO[A]] = (
//    = Status(code = 400),
//    = HttpVersion(major = 1, minor = 1),
//    = Headers(Content-Length: 0),
//    = Entity.Empty,
//    = org.typelevel.vault.Vault@19aa668a
// )

Let's consider Authentication middleware as an example. Authentication middleware is a function that takes AuthedRoutes[F] (that translates to AuthedRequest[F, T] => F[Option[Response[F]]]) and returns HttpRoutes[F] (that translates to Request[F] => F[Option[Response[F]]]). There is a type defined for this in the http4s.server package:

type AuthMiddleware[F[_], T] = Middleware[OptionT[F, *], AuthedRequest[F, T], Response[F], Request[F], Response[F]]

See the Authentication documentation for more details.

Composing Services with Middleware

Since middleware returns a Kleisli, you can compose it with another middleware. Additionally, you can compose services before applying the middleware function, and/or compose services with the service obtained by applying some middleware function. For example:

val apiService = HttpRoutes.of[IO] {
  case GET -> Root / "api" =>
    Ok()
}
// apiService: HttpRoutes[IO] = Kleisli(
//   run = org.http4s.HttpRoutes$$$Lambda$20111/1400366070@4d170761
// )

val anotherService = HttpRoutes.of[IO] {
  case GET -> Root / "another" =>
    Ok()
}
// anotherService: HttpRoutes[IO] = Kleisli(
//   run = org.http4s.HttpRoutes$$$Lambda$20111/1400366070@54ce558
// )

val aggregateService = apiService <+> MyMiddle(service <+> anotherService, "SomeKey" -> "SomeValue")
// aggregateService: Kleisli[cats.data.OptionT[IO, β$0$], Request[IO], Response[IO]] = Kleisli(
//   run = cats.data.KleisliSemigroupK$$Lambda$20112/2135537333@61f824ba
// )

val apiRequest = Request[IO](Method.GET, uri"/api")
// apiRequest: Request[IO] = (
//    = GET,
//    = Uri(
//     scheme = None,
//     authority = None,
//     path = /api,
//     query = ,
//     fragment = None
//   ),
//    = HttpVersion(major = 1, minor = 1),
//    = Headers(),
//    = Entity.Empty,
//    = org.typelevel.vault.Vault@2b2c9039
// )

aggregateService.orNotFound(goodRequest).unsafeRunSync()
// res6: Response[IO[A]] = (
//    = Status(code = 200),
//    = HttpVersion(major = 1, minor = 1),
//    = Headers(Content-Length: 0, SomeKey: SomeValue),
//    = Entity.Empty,
//    = org.typelevel.vault.Vault@5e75a632
// )
aggregateService.orNotFound(apiRequest).unsafeRunSync()
// res7: Response[IO[A]] = (
//    = Status(code = 200),
//    = HttpVersion(major = 1, minor = 1),
//    = Headers(Content-Length: 0),
//    = Entity.Empty,
//    = org.typelevel.vault.Vault@3c71fe81
// )

Note that goodRequest ran through the MyMiddle middleware and the Result had our header added to it. But, apiRequest did not go through the middleware and did not have the header added to it's Result.

Included Middleware

Http4s includes some middleware Out of the Box in the org.http4s.server.middleware package. These include:

And a few others.

Metrics Middleware

Apart from the middleware mentioned in the previous section. There is, as well, Out of the Box middleware for Dropwizard and Prometheus metrics.

X-Request-ID Middleware

Use the RequestId middleware to automatically generate a X-Request-ID header to a request, if one wasn't supplied. Adds a X-Request-ID header to the response with the id generated or supplied as part of the request.

This heroku guide gives a brief explanation as to why this header is useful.

import org.http4s.server.middleware.RequestId
import org.typelevel.ci._

val requestIdService = RequestId.httpRoutes(HttpRoutes.of[IO] {
  case req =>
    val reqId = req.headers.get(ci"X-Request-ID").fold("null")(_.head.value)
    // use request id to correlate logs with the request
    IO(println(s"request received, cid=$reqId")) *> Ok()
})
val responseIO = requestIdService.orNotFound(goodRequest)

Note: req.attributes.lookup(RequestId.requestIdAttrKey) can also be used to lookup the request id extracted from the header, or the generated request id.

// generated request id can be correlated with logs
val resp = responseIO.unsafeRunSync()
// request received, cid=ebb18708-47fc-4473-bf93-f450feb3be14
// resp: Response[IO[A]] = (
//    = Status(code = 200),
//    = HttpVersion(major = 1, minor = 1),
//    = Headers(Content-Length: 0, X-Request-ID: ebb18708-47fc-4473-bf93-f450feb3be14),
//    = Entity.Empty,
//    = org.typelevel.vault.Vault@1c8cc041
// )
// X-Request-ID header added to response
resp.headers
// res8: Headers = Headers(Content-Length: 0, X-Request-ID: ebb18708-47fc-4473-bf93-f450feb3be14)
// the request id is also available using attributes
resp.attributes.lookup(RequestId.requestIdAttrKey)
// res9: Option[String] = Some(value = "ebb18708-47fc-4473-bf93-f450feb3be14")