Our previous blog on http4s gives us an introduction to the library to create HTTP routes, servers and clients. Using the library’s support for various libraries like Ember, Blaze, we can create any type of server and/or client. The library also provides for other libraries for easy encoding/decoding of request and response body using circe, scala-xml, and fs2-data. Ultimately, the heart of the library lies in the type HttpRoutes
, allowing us to create a series of http routes and map different behaviours to it.
Using http4s DSL and supported modules, we can easily create a self-sustained HTTP service, complete with a server and a client. But it’s use case does not just end there. The library allows us to define and reuse a Middleware around our request/response to manipulate it before forwarding it to the server/client respectively. It allows us to add an extra layer to our basic http4s service.
http4s Middleware
A middleware is an abstraction around our service that allows us to manipulate the request sent to it, and/or the response it returns.
Creating a middleware
At its most basic, middleware is a function that takes one service and returns another. Consider a basic scenario where we want to add extra headers to a response before sending it back to the client. A middleware can be easily created to do so as:
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]
. Then, we use a map to get the response out of the task, add headers to it if it is a success, finally passing it on to the client.
Using the middleware
Consider the following scenario where we define a service using HttpRoutes
:
val service = HttpRoutes.of[IO] {
case GET -> Root / "bad" =>
BadRequest()
case _ => Ok()
}
val goodRequest = Request[IO](Method.GET, uri"/")
val badRequest = Request[IO](Method.GET, uri"/bad")
The service works fine if we hit the endpoint "/"
, but a failure if we hit "/bad"
. We can test the service on the REPL itself without using a server or client:
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@59ee59bb
// )
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@e1ec764
// )
Using this implementation, we can apply our middleware that adds headers to a successful response as:
val modifiedService = myMiddle(service, "SomeKey" -> "SomeValue");
Here, modifiedService is a new service with the middleware wrapping the old one. Since it is an http service as well, we can test it too:
modifiedService.orNotFound(goodRequest).unsafeRunSync()
// res2: Response[IO[A]] = (
// = Status(code = 200),
// = HttpVersion(major = 1, minor = 1),
// = Headers(Content-Length: 0, SomeKey: SomeValue),
// = Entity.Empty,
// = org.typelevel.vault.Vault@7eb65acc
// )
modifiedService.orNotFound(badRequest).unsafeRunSync()
// res3: Response[IO[A]] = (
// = Status(code = 400),
// = HttpVersion(major = 1, minor = 1),
// = Headers(Content-Length: 0),
// = Entity.Empty,
// = org.typelevel.vault.Vault@1a3e86a8
// )
In the response generated from a successful hit, we can see that the new headers are present.
Composing Services with Middleware
Since middleware returns a Kleisli
, basically an http service, we can compose it with another middleware. Consider the following:
val apiService = HttpRoutes.of[IO] {
case GET -> Root / "api" =>
Ok()
}
val anotherService = HttpRoutes.of[IO] {
case GET -> Root / "another" =>
Ok()
}
val aggregateService = apiService <+> myMiddle(service <+> anotherService, "SomeKey" -> "SomeValue")
val apiRequest = Request[IO](Method.GET, uri"/api")
Here the aggregateService
is a composition of apiService
(no middleware) and another service we get after applying the middleware myMiddle
to two different services. The output will look like:
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@5c7b0bb
// )
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@6ba27d6d
// )
Included Middleware
http4s includes some middleware out of the box in its codebase including for:
- Authentication: This supports use of Authentication Header as part of the request, or using cookies.
- CORS: This can be used for adding the appropriate headers to responses to allow limited exceptions to this via cross origin resource sharing.
- Response Compression: GZip Middleware can be used to compress the response body using GZip.
- Metrics: We have OOB middleware for Dropwizard and Prometheus metrics.
- X-Request-ID header: We can use the X-Request-ID to automatically generate a X-Request-ID header to a request, if one wasn’t supplied.
Conclusion
http4s Middleware allows us to bring various different layers to our http service, either OOB or by implementing a custom one of our own. Authentication being the most common and important middleware required in any http service, can be easily implemented using http4s codebase. In a separate blog, we will have a look at how to create routes, and how to create authenticated routes using OOB middlware support for authentication.