HandlerAspect
A HandlerAspect
is a wrapper around ProtocolStack
with the two following features:
-
It is a
ProtocolStack
that only works withRequest
andResponse
types. So it is suitable for writing middleware in the context of HTTP protocol. So it can almost be thought of (not the same) as aProtocolStack[Env, Request, Request, Response, Response]]
. -
It is specialized to work with an output context
CtxOut
that can be passed through the middleware stack. This allows each layer to add its output context to the transformation process. So theCtxOut
will be a tuple of all the output contexts that each layer in the stack has added. These output contexts are useful when we are writing middleware that needs to pass some information, which is the result of some computation based on the input request, to the handler that is at the end of the middleware stack.
The diagram below illustrates how HandlerAspect
works:
Now, we are ready to see the definition of HandlerAspect
:
final case class HandlerAspect[-Env, +CtxOut](
protocol: ProtocolStack[Env, Request, (Request, CtxOut), Response, Response]
) extends Middleware[Env] {
def apply[Env1 <: Env, Err](routes: Routes[Env1, Err]): Routes[Env1, Err] = ???
}
Like the ProtocolStack
, the HandlerAspect
is a stack of layers. When we compose two HandlerAspect
using the ++
operator, we are composing handler aspects sequentially. So each layer in the stack corresponds to a separate transformation.
Similar to the ProtocolStack
, each layer in the HandlerAspect
may also be stateful at the level of each transformation. So, for example, a layer that is timing request durations may capture the start time of the request in the incoming interceptor, and pass this state to the outgoing interceptor, which can then compute the duration.
Creating a HandlerAspect
The HandlerAspect
's companion object provides many methods to create a HandlerAspect
. But in this section, we are going to introduce the most basic ones that are used as a building block to create a more complex HandlerAspect
.
The HandlerAspect.identity
is the simplest HandlerAspect
that does nothing. It is useful when you want to create a HandlerAspect
that does not modify the request or response.
After this simple HandlerAspect
, let's dive into the HandlerAspect.intercept*
constructors. Using these, we can create a HandlerAspect
that can intercept the incoming request, outgoing response, or both.
Built-in Handler Aspects
ZIO HTTP offers a versatile set of built-in handler aspects, designed to enhance and customize the handling of HTTP requests and responses. These aspects can be easily integrated into our application to provide various functionalities. For the rest of this page, we will explore how to use them in our applications.
Intercepting
Intercepting the Incoming Requests
The HandlerAspect.interceptIncomingHandler
constructor takes a handler function and applies it to the incoming request. It is useful when we want to modify or access the request before it reaches the handler or the next layer in the stack.
Let's see an example of how to use this constructor to create a handler aspect that checks the IP address of the incoming request and allows only the whitelisted IP addresses to access the server:
import zio._
import zio.http._
val whitelistMiddleware: HandlerAspect[Any, Unit] =
HandlerAspect.interceptIncomingHandler {
val whitelist = Set("127.0.0.1", "0.0.0.0")
Handler.fromFunctionZIO[Request] { request =>
request.headers.get("X-Real-IP") match {
case Some(host) if whitelist.contains(host) =>
ZIO.succeed((request, ()))
case _ =>
ZIO.fail(Response.forbidden("Your IP is banned from accessing the server."))
}
}
}
Intercepting the Outgoing Responses
The HandlerAspect.interceptOutgoingHandler
constructor takes a handler function and applies it to the outgoing response. It is useful when we want to modify or access the response before it reaches the client or the next layer in the stack.
Let's work on creating a handler aspect that adds a custom header to the response:
import zio.http._
val addCustomHeader: HandlerAspect[Any, Unit] =
HandlerAspect.interceptOutgoingHandler(
Handler.fromFunction[Response](_.addHeader("X-Custom-Header", "Hello from Custom Middleware!")),
)
The interceptOutgoingHandler
takes a handler function that receives a Response
and returns a Response
. This is simpler than the interceptIncomingHandler
as it does not necessitate the output context to be passed along with the response.
Intercepting Both Incoming Requests and Outgoing Responses
The HandlerAspect.interceptHandler
takes two handler functions, one for the incoming request and one for the outgoing response.
In the following example, we are going to create a handler aspect that counts the number of incoming requests and outgoing responses and stores them in a Ref
inside the ZIO environment:
import zio._
import zio.http._
def inc(label: String) =
for {
counter <- ZIO.service[Ref[Map[String, Long]]]
_ <- counter.update(_.updatedWith(label) {
case Some(current) => Some(current + 1)
case None => Some(1)
})
} yield ()
val countRequests: Handler[Ref[Map[String, Long]], Nothing, Request, (Request, Unit)] =
Handler.fromFunctionZIO[Request](request => inc("requests").as((request, ())))
val countResponses: Handler[Ref[Map[String, Long]], Nothing, Response, Response] =
Handler.fromFunctionZIO[Response](response => inc("responses").as(response))
val counterMiddleware: HandlerAspect[Ref[Map[String, Long]], Unit] =
HandlerAspect.interceptHandler(countRequests)(countResponses)
Then, we can write another handler aspect that is responsible for adding a route to get the statistics of the incoming requests and outgoing responses:
import zio._
import zio.http._
import zio.schema.codec.JsonCodec.zioJsonBinaryCodec
val statsMiddleware: Middleware[Ref[Map[String, Long]]] =
new Middleware[Ref[Map[String, Long]]] {
override def apply[Env1 <: Ref[Map[String, Long]], Err](routes: Routes[Env1, Err]): Routes[Env1, Err] =
routes ++ Routes(
Method.GET / "stats" -> Handler.fromFunctionZIO[Request] { _ =>
ZIO.serviceWithZIO[Ref[Map[String, Long]]](_.get).map(stats => Response(body = Body.from(stats)))
},
)
}
After attaching these two handler aspects to our Routes
, we have to provide the initial state for the Ref[Map[String, Long]]
to the whole application's environment:
Server.serve(routes @@ counterMiddleware @@ statsMiddleware)
.provide(
Server.default,
ZLayer.fromZIO(Ref.make(Map.empty[String, Long]))
)
Intercepting Statefully
The HandlerAspect.interceptHandlerStateful
constructor is like the interceptHandler
, but it allows the incoming handler to have a state that can be passed to the next layer in the stack, and finally, that state can be accessed by the outgoing handler.
Here is how it works:
- The incoming handler receives a
Request
and produces a tuple ofState
and(Request, CtxOut)
. - The state produced by the incoming handler is passed to the next layer in the stack.
- The outgoing handler receives the
State
along with theResponse
as a tuple, i.e.(State, Response)
, and produces aResponse
.
So, we can pass some state from the incoming handler to the outgoing handler.
In the following example, we are going to write an handler aspect that calculates the response time and includes it in the X-Response-Time
header:
import zio._
import zio.http._
import java.util.concurrent.TimeUnit
val incomingTime: Handler[Any, Nothing, Request, (Long, (Request, Unit))] =
Handler.fromFunctionZIO(request => ZIO.clockWith(_.currentTime(TimeUnit.MILLISECONDS)).map(t => (t, (request, ()))))
val outgoingTime: Handler[Any, Nothing, (Long, Response), Response] =
Handler.fromFunctionZIO { case (incomingTime, response) =>
ZIO
.clockWith(_.currentTime(TimeUnit.MILLISECONDS).map(t => t - incomingTime))
.map(responseTime => response.addHeader("X-Response-Time", s"${responseTime}ms"))
}
val responseTime: HandlerAspect[Any, Unit] =
HandlerAspect.interceptHandlerStateful(incomingTime)(outgoingTime)
By attaching this handler aspect to any route, we can see the response time in the X-Response-Time
header:
$ curl -X GET 'http://127.0.0.1:8080/hello' -i
HTTP/1.1 200 OK
content-type: text/plain
X-Response-Time: 100ms
content-length: 12
Hello World!⏎
Intercepting Statefully (Patching Responses)
Sometimes we want to apply a series of transformations to the outgoing response. We can use the HandlerAspect.interceptPatch
and HandlerAspect.interceptPatchZIO
to achieve this.
A Response.Patch
is a data type that represents a function (or series of functions) that can be applied to a response and return a new response. The HanlderAspect.interceptPatch*
uses this data type to transform the response.
The HandlerApect.interceptPatch
takes two groups of arguments:
- Intercepting the Incoming Request: The first one is a function that takes the incoming
Request
and produces aState
. This state is passed through the handler aspect stack and then can be accessed through the interception phase of the outgoing response. - Intercepting the Outgoing Response: The second one is a function that takes a tuple of
Response
andState
and returns aResponse.Patch
that will be applied to the outgoing response.
Let's try to rewrite the previous example using the HandlerAspect.interceptPatch
:
import zio._
import zio.http._
import java.util.concurrent.TimeUnit
val incomingTime: Request => ZIO[Any, Nothing, Long] =
(_: Request) => ZIO.clockWith(_.currentTime(TimeUnit.MILLISECONDS))
val outgoingTime: (Response, Long) => ZIO[Any, Nothing, Response.Patch] =
(_: Response, incomingTime: Long) =>
ZIO
.clockWith(_.currentTime(TimeUnit.MILLISECONDS).map(t => t - incomingTime))
.map(responseTime => Response.Patch.addHeader("X-Response-Time", s"${responseTime}ms"))
val responseTime: HandlerAspect[Any, Unit] =
HandlerAspect.interceptPatchZIO(incomingTime)(outgoingTime)
Leveraging Output Context
Ordinary Middlewares are intended to bracket a request's execution by intercepting the request, possibly modifying it or short-circuiting its execution, and then performing some post-processing on the response.
However, we sometimes want to gather some contextual information about a request and pass it alongside to the request's handler. This can be achieved with the HandlerAspect[Env, CtxOut]
type, which extends Middleware[Env]
.
The HandlerAspect
middleware produces a value of type CtxOut
on each request, which the routing DSL will accept just like a path component.
If we take a look at the definition of HandlerAspect
, we can see that it has two type parameters, Env
and CtxOut
. The CtxOut
is the output context. When we don't need to pass any context to the output, we use Unit
as the output context, otherwise, we can utilize any type as the output context.
Before diving into a real-world example, let's try to understand the output context with simple examples. First, assume that we have an identity HandlerAspect
that does nothing but passes an integer value to the output context:
import zio._
import zio.http._
val intAspect: HandlerAspect[Any, Int] = HandlerAspect.identity.as(42)
To access this integer value in the handler, we need to define a handler that receives a tuple of (Int, Request)
:
val intRequestHandler: Handler[Int, Nothing, Request, Response] =
Handler.fromFunctionZIO[Request] { (_: Request) =>
ZIO.serviceWith[Int] { n =>
Response.text(s"Received the $n value from the output context!")
}
}
If we attach the intAspect
to this handler, we get back a handler that receives a Request
and produces a Response
:
val handler: Handler[Any, Response, Request, Response] =
intRequestHandler @@ intAspect
Another thing to note is that when we compose multiple HandlerAspect
s with output context of non-Unit
type, the output context of composed HandlerAspect
will be a tuple of all the output contexts:
val stringAspect: HandlerAspect[Any, String] =
HandlerAspect.identity.as("Hello, World!")
val intStringAspect: HandlerAspect[Any, (Int, String)] =
intAspect ++ stringAspect
Correspondingly, to access the output context of this HandlerAspect
, we need to have a handler that receives a tuple of (Int, String, Request)
:
val intStringRequestHandler: Handler[(Int, String), Nothing, Request, Response] =
Handler.fromFunctionZIO[Request] { (req: Request) => ZIO.serviceWith[(Int, String)] { case (n, s) =>
Response.text(s"Received the $n and $s values from the output context!")
}
}
Finally, we can attach the intStringAspect
to this handler:
val handler: Handler[Any, Response, Request, Response] =
intStringRequestHandler @@ (intAspect ++ stringAspect)
Session Example
To look up a Session
, we might use a sessionMiddleware
with type HandlerAspect[Env, Session]
:
Routes(
Method.GET / "user" / int("userId") -> handler {
(userId: Int, request: Request) =>
withContext((session: Session) => UserRepository.getUser(session.organizationId, userId))
}
) @@ sessionMiddleware
The HandlerAspect
companion object provides a number of helpful constructors for these middlewares.
For this example, we would probably use HandlerAspect.interceptHandler
, which wraps an incoming-request handler as well as one which performs any necessary post-processing on the outgoing response:
val incomingHandler: Handler[Env, Response, Request, (Request, Session)] = ???
val outgoingHandler: Handler[Env, Nothing, Response, Response] = ???
HandlerAspect.interceptHandler(incomingHandler)(outgoingHandler)
Note the asymmetry in the type parameters of these two handlers:
- In the incoming case, the handler emits a
Response
on the error-channel whenever the service cannot produce aSession
, effectively short-circuiting the processing of this request. - The outgoing handler, by contrast, has
Nothing
as itsErr
type, meaning that it cannot fail and must always produce aResponse
on the success channel.
Custom Authentication Example
Now, let's see a real-world example where we can leverage the output context.
In the following example, we are going to write an authentication handler aspect that checks the JWT token in the incoming request and passes the user information to the handler:
import zio._
import zio.http._
import scala.util.Try
import pdi.jwt.{Jwt, JwtAlgorithm, JwtClaim}
// Secret Authentication key
val SECRET_KEY = "secretKey"
def jwtDecode(token: String, key: String): Try[JwtClaim] =
Jwt.decode(token, key, Seq(JwtAlgorithm.HS512))
val bearerAuthWithContext: HandlerAspect[Any, String] =
HandlerAspect.interceptIncomingHandler(Handler.fromFunctionZIO[Request] { request =>
request.header(Header.Authorization) match {
case Some(Header.Authorization.Bearer(token)) =>
ZIO
.fromTry(jwtDecode(token.value.asString, SECRET_KEY))
.orElseFail(Response.badRequest("Invalid or expired token!"))
.flatMap(claim => ZIO.fromOption(claim.subject).orElseFail(Response.badRequest("Missing subject claim!")))
.map(u => (request, u))
case _ => ZIO.fail(Response.unauthorized.addHeaders(Headers(Header.WWWAuthenticate.Bearer(realm = "Access"))))
}
})
Now, let's define the /profile/me
route that requires authentication output context:
val profileRoute: Route[Any, Response] =
Method.GET / "profile" / "me" ->
Handler.fromFunctionZIO[Request] { (_: Request) =>
ZIO.serviceWith[String](name => Response.text(s"Welcome $name!"))
} @@ bearerAuthWithContext
That's it! Now, in the handler of the /profile/me
route, we have the username that is extracted from the JWT token inside the authentication handler aspect and passed to it.
The following code snippet is the complete example. Using the login route, we can get the JWT token and use it to access the protected /profile/me
route:
package example
import java.time.Clock
import scala.util.Try
import zio._
import zio.http._
import pdi.jwt.{Jwt, JwtAlgorithm, JwtClaim}
/**
* This is an example to demonstrate bearer Authentication middleware. The
* Server has 2 routes. The first one is for login, Upon a successful login, it
* will return a jwt token for accessing protected routes. The second route is a
* protected route that is accessible only if the request has a valid jwt token.
* AuthenticationClient example can be used to makes requests to this server.
*/
object AuthenticationServer extends ZIOAppDefault {
implicit val clock: Clock = Clock.systemUTC
// Secret Authentication key
val SECRET_KEY = "secretKey"
def jwtEncode(username: String, key: String): String =
Jwt.encode(JwtClaim(subject = Some(username)).issuedNow.expiresIn(300), key, JwtAlgorithm.HS512)
def jwtDecode(token: String, key: String): Try[JwtClaim] =
Jwt.decode(token, key, Seq(JwtAlgorithm.HS512))
val bearerAuthWithContext: HandlerAspect[Any, String] =
HandlerAspect.interceptIncomingHandler(Handler.fromFunctionZIO[Request] { request =>
request.header(Header.Authorization) match {
case Some(Header.Authorization.Bearer(token)) =>
ZIO
.fromTry(jwtDecode(token.value.asString, SECRET_KEY))
.orElseFail(Response.badRequest("Invalid or expired token!"))
.flatMap(claim => ZIO.fromOption(claim.subject).orElseFail(Response.badRequest("Missing subject claim!")))
.map(u => (request, u))
case _ => ZIO.fail(Response.unauthorized.addHeaders(Headers(Header.WWWAuthenticate.Bearer(realm = "Access"))))
}
})
def routes: Routes[Any, Response] =
Routes(
// A route that is accessible only via a jwt token
Method.GET / "profile" / "me" -> handler { (_: Request) =>
ZIO.serviceWith[String](name => Response.text(s"Welcome $name!"))
} @@ bearerAuthWithContext,
// A login route that is successful only if the password is the reverse of the username
Method.GET / "login" ->
handler { (request: Request) =>
val form = request.body.asMultipartForm.orElseFail(Response.badRequest)
for {
username <- form
.map(_.get("username"))
.flatMap(ff => ZIO.fromOption(ff).orElseFail(Response.badRequest("Missing username field!")))
.flatMap(ff => ZIO.fromOption(ff.stringValue).orElseFail(Response.badRequest("Missing username value!")))
password <- form
.map(_.get("password"))
.flatMap(ff => ZIO.fromOption(ff).orElseFail(Response.badRequest("Missing password field!")))
.flatMap(ff => ZIO.fromOption(ff.stringValue).orElseFail(Response.badRequest("Missing password value!")))
} yield
if (password.reverse.hashCode == username.hashCode)
Response.text(jwtEncode(username, SECRET_KEY))
else
Response.unauthorized("Invalid username or password.")
},
) @@ Middleware.debug
override val run = Server.serve(routes).provide(Server.default)
}
After running the server, we can test it using the following client code:
package example
import zio._
import zio.http._
object AuthenticationClient extends ZIOAppDefault {
/**
* This example is trying to access a protected route in AuthenticationServer
* by first making a login request to obtain a jwt token and use it to access
* a protected route. Run AuthenticationServer before running this example.
*/
val url = "http://localhost:8080"
val loginUrl = URL.decode(s"${url}/login").toOption.get
val greetUrl = URL.decode(s"${url}/profile/me").toOption.get
val program = for {
client <- ZIO.service[Client]
// Making a login request to obtain the jwt token. In this example the password should be the reverse string of username.
token <- client
.batched(
Request
.get(loginUrl)
.withBody(
Body.fromMultipartForm(
Form(
FormField.simpleField("username", "John"),
FormField.simpleField("password", "nhoJ"),
),
Boundary("boundary123"),
),
),
)
.flatMap(_.body.asString)
// Once the jwt token is procured, adding it as a Bearer token in Authorization header while accessing a protected route.
response <- client.batched(Request.get(greetUrl).addHeader(Header.Authorization.Bearer(token)))
body <- response.body.asString
_ <- Console.printLine(body)
} yield ()
override val run = program.provide(Client.default)
}
Authentication Handler Aspects
There are several built-in HandlerAspect
s that can be used to implement authentication in ZIO HTTP:
- Basic Authentication: The
basicAuth
andbasicAuthZIO
handler aspect can be used to implement basic authentication. - Bearer Authentication: The
bearerAuth
andbearerAuthZIO
handler aspect can be used to implement bearer authentication. We have to provide a function that validates the bearer token. - Custom Authentication: The
customAuth
andcustomAuthZIO
handler aspects can be used to implement custom authentication. We have to provide a function that validates the request. - Custom Authentication providing: The
customAuthProviding
andcustomAuthProvidingZIO
handler aspects allow us to provide a value to the handler based on the authentication result.
Basic Authentication Example
package example
import zio._
import zio.http.Middleware.basicAuth
import zio.http._
import zio.http.codec.PathCodec.string
object BasicAuth extends ZIOAppDefault {
// Http app that requires basic auth
val user: Routes[Any, Response] = Routes(
Method.GET / "user" / string("name") / "greet" ->
handler { (name: String, _: Request) =>
Response.text(s"Welcome to the ZIO party! ${name}")
},
)
// Add basic auth middleware
val routes: Routes[Any, Response] = user @@ basicAuth("admin", "admin")
val run = Server.serve(routes).provide(Server.default)
}
Custom Authentication Providing Example
package example.middleware
import zio.Config.Secret
import zio._
import zio.http._
import zio.http.codec.PathCodec.string
object CustomAuthProviding extends ZIOAppDefault {
final case class AuthContext(value: String)
// Provides an AuthContext to the request handler
val provideContext: HandlerAspect[Any, AuthContext] = HandlerAspect.customAuthProviding[AuthContext] { r =>
{
r.headers.get(Header.Authorization).flatMap {
case Header.Authorization.Basic(uname, password) if Secret(uname.reverse) == password =>
Some(AuthContext(uname))
case _ =>
None
}
}
}
// Multiple routes that require an AuthContext via withContext
val secureRoutes: Routes[AuthContext, Response] = Routes(
Method.GET / "a" -> handler((_: Request) => withContext((ctx: AuthContext) => Response.text(ctx.value))),
Method.GET / "b" / int("id") -> handler((id: Int, _: Request) =>
withContext((ctx: AuthContext) => Response.text(s"for id: $id: ${ctx.value}")),
),
Method.GET / "c" / string("name") -> handler((name: String, _: Request) =>
withContext((ctx: AuthContext) => Response.text(s"for name: $name: ${ctx.value}")),
),
)
val app: Routes[Any, Response] = secureRoutes @@ provideContext
val run = Server.serve(app).provide(Server.default)
}
To the example, start the server and fire a curl request with an incorrect user/password combination:
curl -i --user admin:wrong http://localhost:8080/user/admin/greet
HTTP/1.1 401 Unauthorized
www-authenticate: Basic
content-length: 0
We notice in the response that first basicAuth
handler aspect responded HTTP/1.1 401 Unauthorized
and then patch handler aspect attached a X-Environment: Dev
header.
Failing HandlerAspects
We can abort the requests by specific response using HandlerAspect.fail
and HandlerAspect.failWith
aspects, so the downstream handlers will not be executed:
import zio.http._
myHandler @@ HandlerAspect.fail(Response.forbidden("Access Denied!"))
myHandler @@ HandlerAspect
.fail(Response.forbidden("Access Denied!"))
.when(req => req.method == Method.DELETE)
Updating Requests and Responses
Several aspects are useful for updating the requests and responses:
Description | HandlerAspect |
---|---|
Update Request | HandlerAspect.updateRequest , HandlerAspect.updateRequestZIO |
Update Request's Method | HandlerAspect.updateMethod |
Update Request's Path | HandlerAspect.updatePath |
Update Request's URL | HandlerAspect.updateURL |
Update Response | HandlerAspect.updateResponse , HandlerAspect.updateResponseZIO |
Update Response Headers | HandlerAspect.updateHeaders |
Update Response Status | HandlerAspect.status |
These aspects can be used to modify the request and response before they reach the handler or the client. They take a function that transforms the request or response and returns the updated request or response. Let's see an example:
val dropTrailingSlash = HandlerAspect.updateURL(_.dropTrailingSlash)
Access Control HandlerAspects
To allow and disallow access to an HTTP based on some conditions, we can use the HandlerAspect.allow
and HandlerAspect.allowZIO
aspects.
val disallow: HandlerAspect[Any, Unit] = HandlerAspect.allow(_ => false)
val allow: HandlerAspect[Any, Unit] = HandlerAspect.allow(_ => true)
val whitelistAspect: HandlerAspect[Any, Unit] = {
val whitelist = Set("127.0.0.1", "0.0.0.0")
HandlerAspect.allow(r =>
r.headers.get("X-Real-IP") match {
case Some(host) => whitelist.contains(host)
case None => false
},
)
}
Cookie Operations
Several aspects are useful for adding, signing, and managing cookies:
HandlerAspect.addCookie
andHandlerAspect.addCookieZIO
to add cookiesHandlerAspect.signCookies
to sign cookiesHandlerAspect.flashScopeHandling
to manage the flash scope
Conditional Application of HandlerAspects
We can attach a handler aspect conditionally using HandlerAspect#when
, HandlerAspect#whenZIO
, and HandlerAspect#whenHeader
methods. Wen also uses the following constructors to have conditional handler aspects: HandlerAspect.when
, HandlerAspect.whenZIO
, HandlerAspect.whenHeader
, HandlerAspect.whenResponse
, and HandlerAspect.whenResponseZIO
.
We have also some if-then-else
style constructors to create conditional aspects like HandlerAspect.ifHeaderThenElse
, HandlerAspect.ifMethodThenElse
, HandlerAspect.ifRequestThenElse
, and HandlerAspect.ifRequestThenElseZIO
.
Request Logging Handler Aspect
The requestLogging
handler aspect is a common aspect that logs incoming requests. It is useful for debugging and monitoring purposes. This aspect logs information such as request method, URL, status code, duration, response and request size by default. We can also configure it to log request and response bodies, request and response headers which are disabled by default:
object HandlerAspect {
def requestLogging(
level: Status => LogLevel = (_: Status) => LogLevel.Info,
loggedRequestHeaders: Set[Header.HeaderType] = Set.empty,
loggedResponseHeaders: Set[Header.HeaderType] = Set.empty,
logRequestBody: Boolean = false,
logResponseBody: Boolean = false,
requestCharset: Charset = StandardCharsets.UTF_8,
responseCharset: Charset = StandardCharsets.UTF_8,
): HandlerAspect[Any, Unit] = ???
}
Running Effect Before/After Every Request
The runBefore
and runAfter
aspects are useful for running an effect before and after every request. These aspects can be used to perform some side effects like logging, metrics and debugging, before and after every request.
Redirect Handler Aspect
There is another handler aspect called HandlerAspect.redirect
which takes a URL
and redirects requests to that URL.
Trailing Slash Handler Aspect
A trailing slash is the last forward-slash character at the end of some URLs. ZIO HTTP have two built-in aspect to handle trailing slashes:
- The
HandlerAspect.redirectTrailingSlash
aspect is useful for redirecting requests with trailing slashes to the same URL without a trailing slash. This aspect is useful for SEO purposes and to avoid duplicate content issues. - The
HandlerAspect.dropTrailingSlash
aspect just drops the trailing slash from the request URL.
Patching Response Handler Aspect
The HandlerAspect.patch
and HandlerAspect.patchZIO
take a function from Request
to Response.Patch
and apply the patch to the response.
Here is an example of a handler aspect that adds a custom header to the response if the request has a specific header:
HandlerAspect.patch(request =>
if (request.hasHeader("X-Foo"))
Response.Patch.addHeader("X-Bar", "Bar Value")
else
Response.Patch.empty
)
Debug Handler Aspect
The debug
handler aspect is a useful aspect for debugging requests and responses. It prints the response status code, request method and url, and the response time of each request to the console.
val helloRoute =
Method.GET / "hello" -> Handler.fromResponse(Response.text("Hello World!")) @@ HandlerAspect.debug
When we send a GET request to the /hello
route, we can see the following output in the console:
200 GET /hello 14ms
Examples
A Simple Middleware Example
Let us consider a simple example using an out-of-the-box handler aspect called addHeader
. We will write an aspect that will attach a custom header to the response.
We create an aspect that appends an additional header to the response indicating whether it is a Dev/Prod/Staging environment:
package example
import java.util.concurrent.TimeUnit
import zio._
import zio.http._
object HelloWorldWithMiddlewares extends ZIOAppDefault {
val routes: Routes[Any, Response] = Routes(
// this will return result instantly
Method.GET / "text" -> handler(ZIO.succeed(Response.text("Hello World!"))),
// this will return result after 5 seconds, so with 3 seconds timeout it will fail
Method.GET / "long-running" -> handler(ZIO.succeed(Response.text("Hello World!")).delay(5 seconds)),
)
val serverTime = Middleware.patchZIO(_ =>
for {
currentMilliseconds <- Clock.currentTime(TimeUnit.MILLISECONDS)
header = Response.Patch.addHeader("X-Time", currentMilliseconds.toString)
} yield header,
)
val middlewares =
// print debug info about request and response
Middleware.debug ++
// close connection if request takes more than 3 seconds
Middleware.timeout(3 seconds) ++
// add static header
Middleware.addHeader("X-Environment", "Dev") ++
// add dynamic header
serverTime
// Run it like any simple app
val run = Server.serve(routes @@ middlewares).provide(Server.default)
}
Fire a curl request, and we see an additional header added to the response indicating the "Dev" environment:
curl -i http://localhost:8080/Bob
HTTP/1.1 200 OK
content-type: text/plain
X-Environment: Dev
content-length: 12
Hello Bob