Skip to main content

Request Handling

Understanding how requests flow through a ZIO HTTP application helps you structure your code and debug issues. This page explains the request lifecycle from arrival to response.

The Request-Response Cycle

When an HTTP request arrives at your server:

  1. The server receives the raw bytes from the network
  2. ZIO HTTP parses the bytes into a Request object
  3. The request is matched against your defined routes
  4. If a route matches, the handler processes the request and produces a Response
  5. If no route matches, a 404 response is generated
  6. The response is serialized and sent back to the client
Client → Network → Server → Route Matching → Handler → Response → Network → Client

Handlers

A Handler is a function that takes some input and produces a Response. The simplest handler ignores the request:

import zio.http._

val simple = handler(Response.text("Hello"))

More typically, handlers inspect the request to produce a response:

import zio.http._

val greeting = handler { (req: Request) =>
val name = req.queryOrElse[String]("name", "World")
Response.text(s"Hello, $name!")
}

Handlers can be effectful, performing database queries, calling external services, or any other ZIO operation:

import zio._
import zio.http._

def getUser(id: String): ZIO[Any, Throwable, String] = ???

val userHandler = handler { (id: String, req: Request) =>
getUser(id).map(user => Response.text(user))
}

Error Handling

Errors in handlers are categorized as:

Handled errors - Errors that have been converted to HTTP responses. These are represented as Response values with appropriate status codes.

Unhandled errors - Errors that haven't been converted to responses yet. Before serving routes, you must handle all errors using methods like handleError or handleErrorCause.

import zio._
import zio.http._

val routes = Routes(
Method.GET / "might-fail" -> handler {
ZIO.fail(new Exception("oops")).map(_ => Response.ok)
}
).handleError { error =>
Response.text(s"Error: ${error.getMessage}").status(Status.InternalServerError)
}

Request Context

Handlers can access contextual information through:

  • Path parameters - Values extracted from the URL path (e.g., /users/:id)
  • Query parameters - Key-value pairs from the URL query string
  • Headers - Request headers for content negotiation, authentication, etc.
  • Body - The request body for POST/PUT/PATCH requests

ZIO HTTP provides typed access to all of these, catching parsing errors before your business logic runs.

The Handler Environment

Handlers can require services from the ZIO environment, allowing dependency injection:

import zio._
import zio.http._

trait UserService {
def findUser(id: String): Task[Option[String]]
}

val userHandler = handler { (id: String, req: Request) =>
ZIO.serviceWithZIO[UserService](_.findUser(id)).map {
case Some(user) => Response.text(user)
case None => Response.notFound
}
}

The required services are tracked in the route's type signature, ensuring all dependencies are provided before the server starts.

For more details on handlers, see the Handler Reference.