Request Handler
A Handler
is responsible for processing the matched incoming request and generating an appropriate response. It is a function that takes a Request
and produces a Response
. Thus, it is a crucial component of the ZIO HTTP that determines how the server should respond to a request matched by the corresponding RoutePattern
.
In ZIO HTTP, each Route
consists of a RoutePattern
and a Handler
. The RoutePattern
is responsible for matching the method and path of the incoming request, while the Handler
specifies the corresponding action to be taken when the request is matched.
Definition
The Hander
trait is defined as follows:
sealed trait Handler[-R, +Err, -In, +Out] {
def apply(in: In): ZIO[R, Err, Out]
}
It has four type parameters. The first two parameters R
and Err
are the environment and error type of the underlying effect that the handler represents. The third and fourth parameters In
and Out
are the input and output types of the handler.
If the input type of the handler is Request
and the output type is Response
, we call that handler a request handler:
type RequestHandler[-R, +Err] = Handler[R, Err, Request, Response]
Creating a Handler
The Handler
trait comes with a companion object that has different methods to create handlers for various needs.
Additionally, there's a smart constructor called handler
in the zio.http
package. It automatically picks the right handler constructor based on the input type. Usually, using handler
is enough, but if we need more control and specificity, we can also use the methods in the Handler
companion object.
Let's look at some examples of creating handlers, using the handler
smart constructor:
import zio._
import zio.http._
Routes(
// 1. A simple handler that returns a "Hello, World!" response
Method.GET / "hello" ->
handler(Response.text("Hello, World!")),
// 2. A handler that echoes the request body
Method.POST / "echo" ->
handler { (req: Request) => req.body.asString(Charsets.Utf8).map(Response.text(_)).orDie },
// 3. A handler that generates a random UUID
Method.GET / "uuid" ->
handler(Random.nextUUID.map(u => Response.text(u.toString))),
// 4. A handler that takes the name from the path and returns a greeting message
Method.GET / "name" / string("name") ->
handler{ (name: String, _: Request) => Response.text(s"Hello, $name!") },
// 5. A handler that takes the name and age from the path and returns birthday greetings
Method.GET / "name" / string("name") / "age" / int("age") ->
handler{ (name: String, age: Int, _: Request) => Response.text(s"Happy $age-th birthday, $name!") }
)
// res0: Routes[Any, Nothing] = Routes(
// routes = IndexedSeq(
// Handled(
// routePattern = RoutePattern(
// method = GET,
// pathCodec = Concat(
// left = Segment(segment = Empty),
// right = Segment(segment = Literal(value = "hello")),
// combiner = zio.http.codec.Combiner$$anon$1@1112f5fd
// )
// ),
// handler = zio.http.Handler$FromFunction$$anon$16@7a6fd5ea,
// location = ""
// ),
// Handled(
// routePattern = RoutePattern(
// method = POST,
// pathCodec = Concat(
// left = Segment(segment = Empty),
// right = Segment(segment = Literal(value = "echo")),
// combiner = zio.http.codec.Combiner$$anon$1@6de5475a
// )
// ),
// handler = zio.http.Handler$FromFunction$$anon$16@65c1db65,
// location = ""
// ),
// Handled(
// routePattern = RoutePattern(
// method = GET,
// pathCodec = Concat(
// left = Segment(segment = Empty),
// right = Segment(segment = Literal(value = "uuid")),
// combiner = zio.http.codec.Combiner$$anon$1@637a673d
// )
// ),
// handler = zio.http.Handler$FromFunction$$anon$16@67be3e3d,
// location = ""
// ),
// Unhandled(
// routePattern = RoutePattern(
// method = GET,
// pathCodec = Concat(
// left = Concat(
// left = Segment(segment = Empty),
// right = Segment(segment = Literal(value = "name")),
// combiner = zio.http.codec.Combiner$$anon$1@20266a07
// ),
// right = Segment(segment = Text(name = "name")),
// combiner = zio.http.codec.Combiner$$anon$1@4bd79795
// ...
Please be aware that this page primarily concentrates on the Handler
data type and its constructors. However, to provide a more comprehensive understanding within the context of routes, we also integrate examples with the Routes
and Method
data types. Detailed exploration of the Routes
and Method
data types will be discussed in a separate section.
As we can see, the handler
constructor is quite versatile and can be used to create handlers for different use cases. It automatically infers proper handler constructors based on the input we pass to it.
-
The first example shows a simple handler that only returns a "Hello, World!" response. It doesn't need any input, so we can directly pass the
Response
to thehandler
constructor. -
The second example shows a handler that echoes the request body. Since it needs the request body, we pass a function that takes a
Request
and returns aResponse
.notePlease note that this handler employs the
orDie
method to transform any failures in the effect into defects. In real-world applications, it's advisable to handle failures more gracefully, such as returning a descriptive error message in the response body. This approach provides clients with a clear understanding of what went wrong. We will delve into error handling in a separate section. -
The third example shows a handler that generates a random UUID. It doesn't need any input, but it requires an effect that produces a
UUID
. So, we pass aZIO
effect that generates a randomUUID
and returns aResponse
. -
The fourth example shows a handler that takes the name from the path and returns a greeting message. It needs the name from the path, so we pass a function that takes a
String
, (and also theRequest
which we ignore it using_
), and returns aResponse
. Please note that whenever we need to access path parameters, we need also to pass theRequest
as an argument to the handler function, even if we don't use it. -
The fifth example is similar to the previous one, but it takes two path parameters.
Handler Constructors
As mentioned earlier, it is advisable to use the handler
smart constructor for convenience. However, in some cases, we might use lower-level handler constructors. Let's look at some of the most commonly used handlers:
Handler.ok
Creates a Handler
that always responds with a 200 status code.
Handler.ok
Succeed/Fail/Die
Like the ZIO
effect, we can create handlers that succeed
, fail
, or die
using the following constructors:
Handler.succeed(42)
// res2: Handler[Any, Nothing, Any, Int] = zio.http.Handler$$anon$13@7ba4525d
Handler.fail(new Error("Server Error!"))
// res3: Handler[Any, Error, Any, Nothing] = zio.http.Handler$$anon$13@6dfdc4c2
Handler.failCause(Cause.fail("Server Error!"))
// res4: Handler[Any, String, Any, Nothing] = zio.http.Handler$$anon$13@7e18ed28
Handler.die(new RuntimeException("Boom!"))
// res5: Handler[Any, Nothing, Any, Nothing] = zio.http.Handler$$anon$13@984a641
Handler.dieMessage("Boom!")
// res6: Handler[Any, Nothing, Any, Nothing] = zio.http.Handler$$anon$13@50eb2b96
Please note that the second type parameter of Handler
is the error type. The succeed
handler doesn't have an error type (Nothing
), the fail
handler has an error type of the given error instance, and the die
handler has no error type (Nothing
) which means it converted the error into a defect.
ZIO boasts a robust error model, enabling us to manage errors in a type-safe and composable manner. If you're unfamiliar with ZIO, it's advisable to explore the Error Management section in the core ZIO documentation. This section provides insights into the principles of error handling within ZIO.
Importing non-ZIO Code
Sometimes we need to import a non ZIO code to a Handler
. The code may throw an exception and we want to capture all non-fatal exceptions while importing to the Handler
, in such cases we can use the Handler.attempt
constructor. It takes a thunk of type Out
and returns a Handler
that have Throwable
as the error type and result type as Out
:
object Handler {
def attempt[Out](out: => Out): Handler[Any, Throwable, Any, Out] = ???
}
Sometimes it becomes necessary to integrate non-ZIO code into a Handler. The external code might be prone to throwing exceptions, so we need to seamlessly incorporate it into Handler while capturing all non-fatal exceptions. By utilizing this constructor, we can encapsulate a thunk of type Out
, resulting in a Handler
where the error type is Throwable
, while maintaining the original Out
as the result type.
From Either
and Exit
If we have an Either
or Exit
which are the result of some computation, we can convert them to a Handler
using the corresponding constructors:
import zio._
import zio.http._
Handler.fromExit(Exit.succeed(42))
// res7: Handler[Any, Nothing, Any, Int] = zio.http.Handler$$anon$13@53b32e9d
Handler.fromExit(Exit.fail("failed!"))
// res8: Handler[Any, String, Any, Nothing] = zio.http.Handler$$anon$13@6afbe86e
Handler.fromEither(Right(42))
// res9: Handler[Any, Nothing, Any, Int] = zio.http.Handler$$anon$13@4661edde
Handler.fromEither(Left("failed"))
// res10: Handler[Any, String, Any, Nothing] = zio.http.Handler$$anon$13@4fdfd91f
First Success
If we have a list of handlers and we want to run them in sequence until the first success, we can use the firstSuccessOf
constructor:
Handler.firstSuccessOf(
NonEmptyChunk(
Handler.notFound("Requested resource not found in cache!"),
Handler.succeed(Response.text("Requested resource found in database!")),
Handler.succeed(Response.text("Requested resource found on the remote server!"))
)
)
// res11: Handler[Any, Nothing, Any, Response] = zio.http.Handler$$anon$4@552c1bf
From ZIO Effect
We can easily convert a ZIO
effect to a Handler
using the fromZIO
constructor:
Handler.fromZIO(ZIO.succeed(42))
// res12: Handler[Any, Nothing, Any, Int] = zio.http.Handler$$anon$14@30dff1e3
Handler.fromZIO(Random.nextUUID)
// res13: Handler[Any, Nothing, Any, java.util.UUID] = zio.http.Handler$$anon$14@7919f53b
From Response
To create a handler that always returns a specific response, we can use the Handler.fromResponse
constructor, or if we have a ZIO
effect that produces a response, we can use the Handler.fromResponseZIO
constructor:
Handler.fromResponse(Response.text("Hello, World!"))
// res14: Handler[Any, Nothing, Any, Response] = zio.http.Handler$$anon$13@483fcc26
Handler.fromResponseZIO(Random.nextUUID.map(u => Response.text(u.toString)))
// res15: Handler[Any, Nothing, Any, Response] = zio.http.Handler$$anon$14@5b73a3a7
From ZIO Stream
ZIO HTTP uses ZIO Streams to handle streaming data. Using Handler.fromStream
and Handler.fromStreamChunked
we can create handlers that produces a response from a ZIO Stream:
Handler.fromStream
- Takes aZStream
and thecontentLength
, and produces aHandler
that returns a response with the given content length from the stream. It waits for the stream to complete before sending the response body. It has two variants, one for producing a response from a stream ofString
and the other one for a stream ofByte
:
object Handler {
def fromStream[R](
stream: ZStream[R, Throwable, String],
contentLength: Long, charset: Charset = Charsets.Http
): Handler[R, Throwable, Any, Response] = ???
def fromStream[R](
stream: ZStream[R, Throwable, Byte],
contentLength: Long
): Handler[R, Throwable, Any, Response] = ???
}
Let's try an example:
import zio.http._
import zio.stream._
Routes(
Method.GET / "stream" ->
Handler
.fromStream(
stream = ZStream
.iterate(0)(_ + 1)
.intersperse("\n")
.map(_.toString)
.schedule(Schedule.fixed(1.second)),
contentLength = 10,
)
)
In this example, when the client sends a GET request to /stream, the server responds with a stream of numbers separated by new lines. The content length of the response is set to 10, leading to the connection closing after the client receives a content body of size 10.
Handler.fromChunkedStream
- Takes aZStream
, and produces aHandler
that returns a chunked response from the stream. It sends the chunks as they are produced by the stream to the client. This is useful for streaming large files or when the content length of the stream is not known in advance. Like thefromStream
constructor, it has two variants:
object Handler {
def fromStreamChunked[R](
stream: ZStream[R, Throwable, String],
charset: Charset = Charsets.Http
): Handler[R, Throwable, Any, Response] = ???
def fromStreamChunked[R](
stream: ZStream[R, Throwable, Byte]
): Handler[R, Throwable, Any, Response] = ???
}
Now, let's try another example, this time using fromStreamChunked
:
import zio.http._
import zio.stream._
Routes(
Method.GET / "stream" ->
Handler
.fromStreamChunked(
ZStream
.iterate(0)(_ + 1)
.intersperse("\n")
.map(_.toString)
.schedule(Schedule.fixed(1.second)),
).orDie
)
In this example, when the client sends a GET request to /stream
, the server responds with a stream of numbers separated by new lines. As the stream produces infinite numbers, the client receives the numbers as they are produced by the server.
From HTML, Text, and Template
Creating a Plain Text Response
The Handler.text
constructor takes a String
and produces a Handler
that returns a response with the given plain text content and the Content-Type
header set to text/plain
:
Handler.text("Hello world!")
Creating an HTML Response
ZIO HTTP has a DSL for creating HTML responses. To use it, we need to import the zio.http.template._
package. The Handler.html
constructor takes an Html
element and produces a Handler
that returns a response with the given HTML content and the Content-Type
header set to text/html
:
import zio.http._
import zio.stream._
import zio.http.template._
Routes(
Method.GET / "html" ->
Handler.html(
html(
// Support for child nodes
head(
title("ZIO HTTP"),
),
body(
div(
// Support for css class names
css := "container text-align-left",
h1("Hello World"),
ul(
// Support for inline css
styles := "list-style: none",
li(
// Support for attributes
a(href := "/hello/world", "Hello World"),
),
li(
a(href := "/hello/world/again", "Hello World Again"),
),
// Support for Seq of Html elements
(2 to 10) map { i =>
li(
a(href := s"/hello/world/i", s"Hello World $i"),
)
},
),
),
),
)
)
)
Creating an HTML Response Using Template
ZIP HTTP has a simple built-in template which is useful for creating simple HTML pages with minimal effort. Let's see an example:
import zio.http._
import zio.http.template._
Routes(
Method.GET / "hello" ->
Handler.template("Hello world!")(
html(
body(
p("This is a simple HTML page.")
)
)
)
)
Let's see what happens if the client requests the above route:
$> curl -i http://127.0.0.1:8080/hello
HTTP/1.1 200 OK
content-type: text/html
content-length: 352
<!DOCTYPE html><html><head><title>ZIO HTTP - Hello world!</title><style>
body {
font-family: monospace;
font-size: 16px;
background-color: #edede0;
}
</style></head><body><div style="margin:auto;padding:2em 4em;max-width:80%"><h1>Hello world!</h1><html><body><p>This is a simple Hello, World! HTML page.</p></body></html></div></body></html>⏎
Timeout Handler
The Handler.timeout
takes a Duration
and returns a Handler
that times out after the given duration with 408
status code.
Status Codes
ZIO HTTP provides a set of constructors for creating handlers that return responses with specific status codes. Here are some of the common ones:
Handler | HTTP Status Code |
---|---|
Handler.ok | 200 |
Handler.badRequest | 400 |
Handler.forbidden | 403 |
Handler.tooLarge | 413 |
Handler.notFound | 404 |
Handler.methodNotAllowed | 405 |
Handler.internalServerError | 500 |
If we need to create a handler that returns a response with a specific status code other than the ones listed above, we can use the Handler.status
constructor.
The Handler.status
constructor creates a Handler
that always responds with the same status code and empty data:
Handler.status(Status.Ok)
Handler.error
Creates a Handler
that always fails with the given error.
Handler.error(Status.Forbidden)
Creating a Handler From Body
The Handler.fromBody
constructor takes a Body
and produces a Handler
that always returns a response with the given body with a 200 status code.
Creating a Bounded Body Consumer
The Handler.asChunkBounded
constructor takes a Request
and the maximum size of the body in bytes (limit
), and produces a Handler
that consumes the body of the request and returns a chunk of bytes. If the body size of the request exceeds the given limit, the handler throws an exception:
import zio.http._
import zio.stream._
import zio.schema.codec.JsonCodec.zioJsonBinaryCodec
Routes(
Method.POST / "bounded-body-consumer" ->
handler { (request: Request) =>
Handler
.asChunkBounded(request, limit = 10)
.map { x: Chunk[Byte] =>
Response(body = Body.fromStream(ZStream.fromChunk(x)))
}.orDie
}.flatten
)
From Function
To create a Handler
using a pure function, use Handler.fromFunction
. It takes a function from In
to Out
(Int => Out
) and returns a Handler
that takes In
and returns Out
(Handler[Any, Nothing, In, Out]
).
The following example shows how to create a handler that takes an Int
and Request
from the input and returns a Response
:
import zio.json._
import zio.http._
Routes(
Method.GET / "users" / int("userId") ->
Handler.fromFunction[(Int, Request)] { case (userId: Int, request: Request) =>
Response.json(
Map(
"user" -> userId.toString,
"correlationId" -> request.headers.get("X-Correlation-ID").get,
).toJsonPretty,
)
}
)
// res24: Routes[Any, Nothing] = Routes(
// routes = IndexedSeq(
// Unhandled(
// routePattern = RoutePattern(
// method = GET,
// pathCodec = Concat(
// left = Concat(
// left = Segment(segment = Empty),
// right = Segment(segment = Literal(value = "users")),
// combiner = zio.http.codec.Combiner$$anon$1@146c11c3
// ),
// right = Segment(segment = IntSeg(name = "userId")),
// combiner = zio.http.codec.Combiner$$anon$1@226a8e58
// )
// ),
// handler = zio.http.Handler$FromFunction$$anon$16@148238cf,
// zippable = zio.ZippableLowPriority3$$anon$23@522acf63,
// location = "repl.MdocSession.MdocApp.res24(handler.md:324)"
// )
// )
// )
The Handler.fromFunction
has some variants that are useful in different scenarios:
Constructor | Input Function | Output |
---|---|---|
Handler.fromFunction | In => Out | Handler[Any, Nothing, In, Out] |
Handler.fromFunctionHandler | In => Handler[R, Err, In, Out] | Handler[R, Err, In, Out] |
Handler.fromFunctionExit | In => Exit[Err, Out] | Handler[Any, Err, In, Out] |
Handler.fromFunctionZIO | In => ZIO[R, Err, Out] | Handler[R, Err, In, Out] |
From File
The Handler.fromFile
and Handler.fromFileZIO
constructors are used to create handlers that return a file from the server:
Constructor | Input Function | Output |
---|---|---|
Handler.fromFile | File | Handler[R, Throwable, Any, Response] |
Handler.fromFileZIO | ZIO[R, Throwable, File] | Handler[R, Throwable, Any, Response] |
Let's see an example:
import zio.http._
import java.io.File
Routes(
Method.GET / "video" ->
Handler.fromFile(new File("src/main/resources/TestVideoFile.mp4")),
Method.GET / "text" ->
Handler.fromFile(new File("src/main/resources/TestFile.txt")),
)
Parameter Extractor
The Handler.param
is a builder that takes a type parameter A
and returns a ParamExtractorBuilder[A]
which is used to extract a parameter from the input. It is useful when we have set of small handlers that are working with only part of the request, so we can extract the part that is required and pass it to the corresponding handler. All these handlers have unified input but may have different output types; this is where we can easily combine them using monadic composition.
Here is an example:
import zio.http._
Routes(
Method.GET / "static" / trailing -> handler {
// Path extractor
val pathExtractor: Handler[Any, Nothing, (Path, Request), Path] =
Handler.param[(Path, Request)](_._1)
// Request extractor
val requestExtractor: Handler[Any, Nothing, (Path, Request), Request] =
Handler.param[(Path, Request)](_._2)
def logRequest(request: Request): Handler[Any, Throwable, Request, Response] = ???
def staticFileHandler(path: Path): Handler[Any, Throwable, Request, Response] = ???
for {
path <- pathExtractor
request <- requestExtractor
_ <- logRequest(request).contramap[(Path,Request)](_._2)
resp <- staticFileHandler(path).contramap[(Path, Request)](_._2)
} yield (resp)
}.sandbox
)
Websocket
The Handler.webSocket
constructor takes a function of type WebSocketChannel => ZIO[Env, Throwable, Any]
and returns a Handler
that handles the WebSocket requests.
object Handler {
final def webSocket[Env](
f: WebSocketChannel => ZIO[Env, Throwable, Any],
): WebSocketApp[Env] = ???
}
The following example shows how to create an echo server using the Handler.webSocket
constructor:
import zio.http._
import zio.http.ChannelEvent._
Routes(
Method.GET / "websocket" ->
handler {
Handler.webSocket { channel =>
channel.receiveAll {
case Read(WebSocketFrame.Text(text)) =>
channel.send(Read(WebSocketFrame.Text(text)))
case _ =>
ZIO.unit
}
}.toResponse
}
)
To be able to create a complete route, we need to convert the WebSocketApp
to a Response
using the toResponse
method.
Stack Trace
By using Handler.stackTrace
we can create a Handler
that captures the ZIO stack trace at the current point:
object Handler {
def stackTrace(implicit trace: Trace): Handler[Any, Nothing, Any, StackTrace] =
fromZIO(ZIO.stackTrace)
}
Let's try an example:
import zio.http._
Routes(
Method.GET / "stacktrace" ->
handler {
for {
stack <- Handler.stackTrace
} yield Response.text(stack.prettyPrint)
}
)
Returning a full stack trace in the body of an HTTP response is generally not recommended for production environments. Stack traces can contain sensitive information about your application's internals, which could be exploited by attackers.
A better practice is to log the stack trace on the server side for debugging purposes and return a more user-friendly error message to the client. This approach provides clients with a clear understanding of what went wrong. We will delve into error handling in a separate section.
Handler Operators
LikeZIO
data type, the Handler
has various operators for various operators for handling errors, timing out, combining handlers, maping
Handler Aspect
To attach a handler aspect to a handler, we use the @@
operator. For instance, the following code shows an example where we attach a logging handler to the echo handler:
import zio.http._
Routes(
Method.GET / "echo" -> handler { req: Request =>
Handler.fromBody(req.body)
}.flatten @@ HandlerAspect.requestLogging()
)
This will log every request coming to these handlers. ZIO HTTP supports various HandlerAspects
that you can learn about in the Middleware section.
Sandboxing Errors
The Handler#sandbox
operator described is a potentially time-saving solution for managing errors within an HTTP application. Its primary function is the elimination of errors by translating them into an error of type Response
, allowing developers to transition into a controlled environment where errors are effectively mitigated:
sealed trait Handler[-R, +Err, -In, +Out] { self =>
def sandbox: Handler[R, Response, In, Out]
}
This tool could serve as a shortcut for developers who wish to bypass the complication of error handling, enabling them to focus more on other aspects of their code.
Let's see an example:
import zio.http._
import java.nio.file._
Routes(
Method.GET / "file" ->
Handler.fromFile(Paths.get("file.txt").toFile).sandbox,
)
In this example, the type of the handler before applying the sandbox
operator is Handler[Any, Throwable, Any, Response]
. After applying the sandbox
operator, the type of the handler becomes Handler[Any, Response, Any, Response]
.
Without the sandbox
operator, the compiler would complain about the unhandled Throwable
error.
Converting a Handler
to an Routes
The Handler#toRoutes
operator, converts a handler to an Routes
to be served by the Server
. The following example, shows an HTTP application that serves a simple "Hello, World!" response for all types of incoming requests:
import zio._
import zio.http._
object HelloWorldServer extends ZIOAppDefault {
def run =
Server
.serve(Handler.fromResponse(Response.text("Hello, world!")).toRoutes)
.provide(Server.default)
}
Response Projections
The Handler#header
, Handler#headers
, Handler#status
, and Handler#body
operators are used to extract specific parts of the response from a handler's output:
trait Handler[-R, +Err, -In, +Out] {
def header(headerType: HeaderType)(
implicit ev: Out <:< Response
): Handler[R, Err, In, Option[headerType.HeaderValue]]
def headers(implicit ev: Out <:< Response): Handler[R, Err, In, Headers]
def status(implicit ev: Out <:< Response, trace: Trace): Handler[R, Err, In, Status]
def body(implicit trace: Trace): Handler[R, Err, In, Body]
}
Running Handler
We know that a handler is just a function that takes an input and returns an output:
trait Handler[-R, +Err, -In, +Out] {
def apply(in: In): ZIO[R, Err, Out]
}
So, to run a handler, we just need to call the apply
method with the required input, and it will return a ZIO
effect. The Handler#runZIO
is an alternative to the Handler#apply
method.
In cases where the input type of handler is Request
, we can use the Handler#run
method:
trait Handler[-R, +Err, -In, +Out] {
def run(
method: Method = Method.GET,
path: Path = Path.root,
headers: Headers = Headers.empty,
body: Body = Body.empty,
)(implicit ev: Request <:< In): ZIO[R, Err, Out]
}
Mapping
Like ZIO
data type, the Handler
has various operators for mapping the input and output types, and error types:
Operator | Explanation | Variations |
---|---|---|
Handler#map* | Used to transform the output of a handler. | map , mapError , mapErrorCause , mapZIO , mapErrorZIO , mapErrorCauseZIO |
Handler#contramap* | Used to transform the input of a handler. | contramap , contramapZIO |
Handler#mapError* | Used to transform the error type of a handler. | mapError , mapErrorCause , mapErrorZIO , mapErrorCauseZIO |
If you're unfamiliar with these operators, it's recommended to explore the core ZIO documentation for a deeper understanding of their functionality.
Composing Handlers
-
flatMap — This function is utilized to compose two handlers in scenarios where we aim to employ monadic composition. It combines the output of the first handler with the input of the second handler. The
>>=
operator serves as an alias forflatMap
. -
andThen — Employed to sequentially compose a handler with another handler, without the need for monadic composition. Given two handlers,
h1: A => B
andh2: B => C
, the resulting functionA => C
is achieved throughh1 andThen h2
. The>>>
operator acts as an alias forandThen
. -
compose — This function functions as the inverse of
andThen
. When provided with two handlers,h1: B => C
andh2: A => B
, it returns the functionA => C
viah1 compose h2
. The<<<
operator is an alias forcompose
. -
zip — Utilized to merge two handlers into a singular handler, producing a tuple comprising the outputs of both handlers.
zipLeft
andzipRight
are utilized when only one handler's output is of interest. Thezip
operator is alias forzip
, while<*
and*>
serve as aliases forzipLeft
andzipRight
respectively. -
orElse — This function combines two handlers into one, attempting the first handler and proceeding to the second if the first one fails. The
<>
operator is an alias fororElse
.
Operator | Explanation | Variations |
---|---|---|
flatMap | Used to compose two handlers using monadic composition. | flatMap , >>= |
andThen | Feed the output of the first handler to input of the next handler. | andThen , >>> |
compose | Feed the output of the next handler to input of the first handler. | compose , <<< |
zip | Combines two handlers into one returning a tuple of outputs. | zip , zipLeft , zipRight , <*> <* , *> |
orElse | Combines two handlers, trying the first and then the second. | orElse , <> |
Error Management
Like ZIO
data type, the Handler
has various operators for handling errors, such as orDie*
, refineOrDie
, catchAll*
, unrefine*
.
The are similar to the ZIO
ones, but they are specialized for the Handler
type. If you're unfamiliar with these operators, it's recommended to explore the Error Management section in the core ZIO documentation.
Working with Environment and Layers
The first type parameter of the Handler
is the environment type. This means that a Handler
can require an environment to run, like a ZIO
effect. When we create a Handler
, we can get access to the environment using ZIO.service*
methods, and finally, we can provide the environment using Handler#provide*
methods.
Please note that in most cases, we are not required to provide the environment of the handler in the middle of the routes definition. It is usually done at the end when we are creating the Routes
using the Server#serve
method.
If you are not familiar with the concept of environment and layers in ZIO, it is recommended to explore the Introduction to the ZIO's Contextual Data Types section in the core ZIO documentation.
Adding Delays
When we have andler of type In => Out
, we can delay the consumption of the input by using the Handler#delayBefore
and delay the production of the output by using the Handler#delayAfter
.
Converting to Constant Values/Types
To convert a Handler
to a constant value, we can use the Handler#as
method. It takes a value of type Out
and returns a Handler
that always returns the given value.
We can also narrow or widen the type of environment, error, input, or output of a Handler
using the Handler#asEnvType
, Handler#asErrorType
, Handler#asInType
, and Handler#asOutType
methods.
Overwriting the Method
The Handler.method
overwrites the method in the incoming request to the Handler
:
val handler11 = Handler.fromFunction((request: Request) => Response.text(request.method.toString))
handler11.method(Method.POST)
Patching the Response
The Handler.patch
patches the response produced by the request handler using a Patch
:
val handler12 = Handler.fromResponse(Response.text("Hello World!"))
val handler13 = handler12.patch(Response.Patch.status(Status.Accepted))