Skip to main content
Version: 2.x

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.

note

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
// ...
note

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.

  1. 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 the handler constructor.

  2. 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 a Response.

    note

    Please 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.

  3. 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 a ZIO effect that generates a random UUID and returns a Response.

  4. 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 the Request which we ignore it using _), and returns a Response. Please note that whenever we need to access path parameters, we need also to pass the Request as an argument to the handler function, even if we don't use it.

  5. 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.

info

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 a ZStream and the contentLength, and produces a Handler 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 of String and the other one for a stream of Byte:
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 a ZStream, and produces a Handler 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 the fromStream 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:

HandlerHTTP Status Code
Handler.ok200
Handler.badRequest400
Handler.forbidden403
Handler.tooLarge413
Handler.notFound404
Handler.methodNotAllowed405
Handler.internalServerError500

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:

ConstructorInput FunctionOutput
Handler.fromFunctionIn => OutHandler[Any, Nothing, In, Out]
Handler.fromFunctionHandlerIn => Handler[R, Err, In, Out]Handler[R, Err, In, Out]
Handler.fromFunctionExitIn => Exit[Err, Out]Handler[Any, Err, In, Out]
Handler.fromFunctionZIOIn => 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:

ConstructorInput FunctionOutput
Handler.fromFileFileHandler[R, Throwable, Any, Response]
Handler.fromFileZIOZIO[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
}
)
note

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)
}
)
note

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:

OperatorExplanationVariations
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
note

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

  1. 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 for flatMap.

  2. andThen — Employed to sequentially compose a handler with another handler, without the need for monadic composition. Given two handlers, h1: A => B and h2: B => C, the resulting function A => C is achieved through h1 andThen h2. The >>> operator acts as an alias for andThen.

  3. compose — This function functions as the inverse of andThen. When provided with two handlers, h1: B => C and h2: A => B, it returns the function A => C via h1 compose h2. The <<< operator is an alias for compose.

  4. zip — Utilized to merge two handlers into a singular handler, producing a tuple comprising the outputs of both handlers. zipLeft and zipRight are utilized when only one handler's output is of interest. The zip operator is alias for zip, while <* and *> serve as aliases for zipLeft and zipRight respectively.

  5. 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 for orElse.

OperatorExplanationVariations
flatMapUsed to compose two handlers using monadic composition.flatMap, >>=
andThenFeed the output of the first handler to input of the next handler.andThen, >>>
composeFeed the output of the next handler to input of the first handler.compose, <<<
zipCombines two handlers into one returning a tuple of outputs.zip, zipLeft, zipRight, <*> <*, *>
orElseCombines 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.

note

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.

note

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))