Middleware
A middleware is a wrapper 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 simply a function that takes one Service
as a
parameter and returns another Service
. In addition to the Service
, 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._
Then, we can create a middleware that adds a header to successful responses from
the wrapped service like this.
def myMiddle(service: HttpRoutes[IO], header: Header): 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 Service
is implemented as a Kleisli
, which is just a function at heart, we can test a
service without a server. Because an HttpService[F]
returns a F[Response[F]]
,
we need to call unsafeRunSync
on the result of the function to extract the Response[F]
.
val service = HttpRoutes.of[IO] {
case GET -> Root / "bad" =>
BadRequest()
case _ =>
Ok()
}
// service: HttpRoutes[IO] = Kleisli(
// org.http4s.HttpRoutes$$$Lambda$8265/1868696275@471aba62
// )
val goodRequest = Request[IO](Method.GET, uri"/")
// goodRequest: Request[IO] = (
// Method("GET"),
// Uri(None, None, "/", , None),
// HttpVersion(1, 1),
// Headers(),
// Stream(..),
// io.chrisdavenport.vault.Vault@35bdcdae
// )
val badRequest = Request[IO](Method.GET, uri"/bad")
// badRequest: Request[IO] = (
// Method("GET"),
// Uri(None, None, "/bad", , None),
// HttpVersion(1, 1),
// Headers(),
// Stream(..),
// io.chrisdavenport.vault.Vault@4fa1c10d
// )
service.orNotFound(goodRequest).unsafeRunSync()
// res0: Response[IO] = Response(
// Status(200),
// HttpVersion(1, 1),
// Headers(Content-Length: 0),
// Stream(..),
// io.chrisdavenport.vault.Vault@71e3bdb8
// )
service.orNotFound(badRequest).unsafeRunSync()
// res1: Response[IO] = Response(
// Status(400),
// HttpVersion(1, 1),
// Headers(Content-Length: 0),
// Stream(..),
// io.chrisdavenport.vault.Vault@63b8d04c
// )
Now, we’ll wrap the service in our middleware to create a new service, and try it out.
val wrappedService = myMiddle(service, Header("SomeKey", "SomeValue"));
// wrappedService: HttpRoutes[IO] = Kleisli(<function1>);
wrappedService.orNotFound(goodRequest).unsafeRunSync()
// res2: Response[IO] = Response(
// Status(200),
// HttpVersion(1, 1),
// Headers(Content-Length: 0, SomeKey: SomeValue),
// Stream(..),
// io.chrisdavenport.vault.Vault@3833a128
// )
wrappedService.orNotFound(badRequest).unsafeRunSync()
// res3: Response[IO] = Response(
// Status(400),
// HttpVersion(1, 1),
// Headers(Content-Length: 0),
// Stream(..),
// io.chrisdavenport.vault.Vault@795bb68a
// )
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) =
resp match {
case Status.Successful(resp) => resp.putHeaders(header)
case resp => resp
}
def apply(service: HttpRoutes[IO], header: Header) =
service.map(addHeader(_, header))
}
val newService = MyMiddle(service, Header("SomeKey", "SomeValue"))
// newService: Kleisli[cats.data.OptionT[IO, β$0$], Request[IO], Response[IO]] = Kleisli(
// cats.data.Kleisli$$Lambda$9238/1213554820@1539ca64
// )
newService.orNotFound(goodRequest).unsafeRunSync()
// res4: Response[IO] = Response(
// Status(200),
// HttpVersion(1, 1),
// Headers(Content-Length: 0, SomeKey: SomeValue),
// Stream(..),
// io.chrisdavenport.vault.Vault@3026423f
// )
newService.orNotFound(badRequest).unsafeRunSync()
// res5: Response[IO] = Response(
// Status(400),
// HttpVersion(1, 1),
// Headers(Content-Length: 0),
// Stream(..),
// io.chrisdavenport.vault.Vault@15399ff2
// )
It is possible for the wrapped Service
to have different Request
and Response
types than the middleware. Authentication is, again, a good example. Authentication
middleware is an HttpService
(an alias for Service[Request, Response]
) that wraps an
AuthedService
(an alias for Service[AuthedRequest[T], Response]
. There is a type
defined for this in the http4s.server
package:
type AuthMiddleware[F, T] = Middleware[AuthedRequest[F, T], Response[F], Request[F], Response[F]]
See the Authentication documentation for more details.
Composing Services with Middleware
Because middleware returns a Service
, you can compose services wrapped in
middleware with other, unwrapped, services, or services wrapped in other middleware.
You can also wrap a single service in multiple layers of middleware. For example:
val apiService = HttpRoutes.of[IO] {
case GET -> Root / "api" =>
Ok()
}
// apiService: HttpRoutes[IO] = Kleisli(
// org.http4s.HttpRoutes$$$Lambda$8265/1868696275@31729439
// )
val aggregateService = apiService <+> MyMiddle(service, Header("SomeKey", "SomeValue"))
// aggregateService: Kleisli[cats.data.OptionT[IO, β$0$], Request[IO], Response[IO]] = Kleisli(
// cats.data.KleisliSemigroupK$$Lambda$8266/1814673590@6ac30299
// )
val apiRequest = Request[IO](Method.GET, uri"/api")
// apiRequest: Request[IO] = (
// Method("GET"),
// Uri(None, None, "/api", , None),
// HttpVersion(1, 1),
// Headers(),
// Stream(..),
// io.chrisdavenport.vault.Vault@68f68244
// )
aggregateService.orNotFound(goodRequest).unsafeRunSync()
// res6: Response[IO] = Response(
// Status(200),
// HttpVersion(1, 1),
// Headers(Content-Length: 0, SomeKey: SomeValue),
// Stream(..),
// io.chrisdavenport.vault.Vault@31e946a7
// )
aggregateService.orNotFound(apiRequest).unsafeRunSync()
// res7: Response[IO] = Response(
// Status(200),
// HttpVersion(1, 1),
// Headers(Content-Length: 0),
// Stream(..),
// io.chrisdavenport.vault.Vault@141fdfff
// )
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
Dropwizard Metrics Middleware
To make use of this metrics middleware the following dependencies are needed:
libraryDependencies ++= Seq(
"org.http4s" %% "http4s-server" % http4sVersion,
"org.http4s" %% "http4s-dropwizard-metrics" % http4sVersion
)
We can create a middleware that registers metrics prefixed with a
provided prefix like this.
import org.http4s.server.middleware.Metrics
import org.http4s.metrics.dropwizard.Dropwizard
import com.codahale.metrics.SharedMetricRegistries
implicit val clock = Clock.create[IO]
// clock: Clock[IO] = cats.effect.Clock$$anon$1@66bad02f
val registry = SharedMetricRegistries.getOrCreate("default")
// registry: com.codahale.metrics.MetricRegistry = com.codahale.metrics.MetricRegistry@60cbbebc
val meteredRoutes = Metrics[IO](Dropwizard(registry, "server"))(apiService)
// meteredRoutes: HttpRoutes[IO] = Kleisli(
// org.http4s.server.middleware.BracketRequestResponse$$$Lambda$9556/2138371415@3a828a1e
// )
Prometheus Metrics Middleware
To make use of this metrics middleware the following dependencies are needed:
libraryDependencies ++= Seq(
"org.http4s" %% "http4s-server" % http4sVersion,
"org.http4s" %% "http4s-prometheus-metrics" % http4sVersion
)
We can create a middleware that registers metrics prefixed with a
provided prefix like this.
import cats.effect.{Clock, IO, Resource}
import org.http4s.HttpRoutes
import org.http4s.metrics.prometheus.{Prometheus, PrometheusExportService}
import org.http4s.server.Router
import org.http4s.server.middleware.Metrics
implicit val clock = Clock.create[IO]
// clock: Clock[IO] = cats.effect.Clock$$anon$1@41a52f97
val meteredRouter: Resource[IO, HttpRoutes[IO]] =
for {
metricsSvc <- PrometheusExportService.build[IO]
metrics <- Prometheus.metricsOps[IO](metricsSvc.collectorRegistry, "server")
router = Router[IO](
"/api" -> Metrics[IO](metrics)(apiService),
"/" -> metricsSvc.routes
)
} yield router
// meteredRouter: Resource[IO, HttpRoutes[IO]] = Bind(
// Bind(
// Allocate(
// Map(
// Map(
// Delay(
// org.http4s.metrics.prometheus.Prometheus$$$Lambda$8894/2038568608@32a3a435
// ),
// cats.effect.Resource$$$Lambda$8403/897991529@2b2ff07b,
// StackTrace(
// List(
// cats.effect.internals.IOTracing$.buildFrame(IOTracing.scala:48),
// cats.effect.internals.IOTracing$.buildCachedFrame(IOTracing.scala:39),
// cats.effect.internals.IOTracing$.cached(IOTracing.scala:34),
// cats.effect.IO.map(IO.scala:106),
// cats.effect.IOLowPriorityInstances$IOEffect.map(IO.scala:870),
// cats.effect.IOLowPriorityInstances$IOEffect.map(IO.scala:863),
// cats.Functor$Ops.map(Functor.scala:233),
// cats.Functor$Ops.map$(Functor.scala:233),
// cats.Functor$ToFunctorOps$$anon$4.map(Functor.scala:250),
// cats.effect.Resource$.make(Resource.scala:342),
// org.http4s.server.blaze.BlazeServerBuilder.$anonfun$resource$1(BlazeServerBuilder.scala:412),
// cats.effect.Resource.$anonfun$fold$1(Resource.scala:124),
// cats.effect.internals.IOBracket$BracketStart.liftedTree1$1(IOBracket.scala:95),
// cats.effect.internals.IOBracket$BracketStart.run(IOBracket.scala:95),
// cats.effect.internals.Trampoline.cats$effect$internals$Trampoline$$immediateLoop(Trampoline.scala:67),
// cats.effect.internals.Trampoline.startLoop(Trampoline.scala:35),
// cats.effect.internals.TrampolineEC$JVMTrampoline.super$startLoop(TrampolineEC.scala:90),
// cats.effect.internals.TrampolineEC$JVMTrampoline.$anonfun$startLoop$1(TrampolineEC.scala:90),
// scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:23),
// scala.concurrent.BlockContext$.withBlockContext(BlockContext.scala:85),
// cats.effect.internals.TrampolineEC$JVMTrampoline.startLoop(TrampolineEC.scala:90),
// cats.effect.internals.Trampoline.execute(Trampoline.scala:43),
// cats.effect.internals.TrampolineEC.execute(TrampolineEC.scala:42),
// cats.effect.internals.IOBracket$BracketStart.apply(IOBracket.scala:80),
// cats.effect.internals.IOBracket$BracketStart.apply(IOBracket.scala:58),
// ...
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.http4s.util.CaseInsensitiveString
val requestIdService = RequestId.httpRoutes(HttpRoutes.of[IO] {
case req =>
val reqId = req.headers.get(CaseInsensitiveString("X-Request-ID")).fold("null")(_.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=be551e0e-5a5e-4dd9-bbf6-489c76d585ea
// resp: Response[IO] = Response(
// Status(200),
// HttpVersion(1, 1),
// Headers(Content-Length: 0, X-Request-ID: be551e0e-5a5e-4dd9-bbf6-489c76d585ea),
// Stream(..),
// io.chrisdavenport.vault.Vault@1b247699
// )
// X-Request-ID header added to response
resp.headers
// res8: Headers = Headers(Content-Length: 0, X-Request-ID: be551e0e-5a5e-4dd9-bbf6-489c76d585ea)
// the request id is also available using attributes
resp.attributes.lookup(RequestId.requestIdAttrKey)
// res9: Option[String] = Some("be551e0e-5a5e-4dd9-bbf6-489c76d585ea")