Skip to content

Request

The Request is an immutable case class that carries all information about an incoming HTTP request.

case class Request(
method: String, // HTTP method (GET, POST, etc.)
url: String, // Full URL including query string
path: String, // URL path only
headers: Map[String, String], // Request headers (lowercase keys)
params: Map[String, String], // Path parameters from route
query: Map[String, Seq[String]], // Query string parameters (multi-valued)
context: Context, // Type-safe extensible context for middleware data
rawRequest: ServerRequest, // Underlying Node.js request object
basePath: String, // Accumulated base path from subrouters
finalizers: List[Finalizer], // Response transformers (LIFO)
cookies: Map[String, String], // Parsed cookies
)

Extract named segments from the route pattern:

server.get("/users/:id/posts/:postId", request => {
val userId = request.params("id")
val postId = request.params("postId")
s"User $userId, Post $postId".asText
})

Access query string values:

Query strings are multi-valued (repeated keys are preserved). Use queryParam for the first value, or index query for all values of a key:

// GET /search?q=scala&page=2&tag=fp&tag=web
server.get("/search", request => {
val q = request.queryParam("q").getOrElse("") // first value: Option[String]
val page = request.queryParam("page").map(_.toInt).getOrElse(1)
val tags = request.query.getOrElse("tag", Nil) // all values: Seq[String]
s"Searching '$q' page $page tags ${tags.mkString(",")}".asText
})

Headers are stored with lowercase keys for case-insensitive access:

val token = request.header("authorization")
.filter(_.startsWith("Bearer "))
.map(_.substring(7))
val contentType = request.header("content-type")

Request body is read lazily as a stream. Four methods are available:

request.body.flatMap { buffer: Buffer =>
// Process binary data
buffer.asBinary
}

Handles charset detection from the Content-Type header:

request.text.flatMap { text: String =>
s"Received: $text".asText
}

Type-safe parsing with zio-json. Define your types with derives:

case class User(name: String, email: String) derives JsonDecoder
request.json[User].flatMap {
case Some(user) => user.asJson(201)
case None => "Invalid JSON".asText(400)
}

URL-encoded form bodies (application/x-www-form-urlencoded):

Form bodies are multi-valued too (Map[String, Seq[String]]); formField returns the first value of a field:

request.form.flatMap { formData: Map[String, Seq[String]] =>
val username = formData.get("username").flatMap(_.headOption).getOrElse("")
val password = formData.get("password").flatMap(_.headOption).getOrElse("")
processLogin(username, password)
}

Configure maximum body size and read timeout:

// Per-server defaults via ServerConfig
val server = Server(ServerConfig(
maxBodySize = 50 * 1024 * 1024, // 50 MB (default)
bodyTimeout = 30000, // 30 seconds (default)
))

For per-route limits, use BodyLimitMiddleware:

server.post("/upload", BodyLimitMiddleware(10 * 1024 * 1024), uploadHandler)
request.ip // Remote IP address (String)
request.hostname // Host header value (Option[String])
request.port // Remote port (Option[Int])
request.protocol // "http" or "https" (String)
request.secure // true if HTTPS (Boolean)
request.httpVersion // HTTP version string
request.complete // Whether the request has been fully received
request.aborted // Whether the client aborted the connection
// Access a cookie by name
request.cookie("session") // Option[String]
// All cookies
request.cookies // Map[String, String]

With CookieMiddleware enabled, additional methods are available:

request.getSignedCookie("auth") // Verified signed cookie
request.getJsonCookie[Settings]("prefs") // Parse JSON cookie

The context is a type-safe, immutable store (Context) that middleware use to pass data to downstream handlers. Values are keyed by a TypedKey[A], so reads recover the value’s type with no casting — define each key once and share it between the writer and the reader:

// Define a key (typically a val in a companion object)
val UserKey: TypedKey[User] = TypedKey("user")
// Middleware adds data
val withUser: Handler = request => {
val user = lookupUser(request.params("id"))
Future.successful(Continue(
request.copy(context = request.context.updated(UserKey, user))
))
}
// Handler reads it — typed as Option[User], no cast needed
val handler: Handler = request =>
request.context.get(UserKey) match {
case Some(user) => user.asJson
case None => failNotFound("User not found")
}

AuthMiddleware stores its Auth under AuthMiddleware.authKey:

request.context.get(AuthMiddleware.authKey) match {
case Some(auth) => println(s"User: ${auth.user}, Roles: ${auth.roles}")
case None => // Not authenticated
}

Attach response transformers that run after a Complete result:

val modifiedRequest = request.addFinalizer { (req, response) =>
Future.successful(response.copy(
headers = response.headers.add("X-Custom", "value")
))
}

Finalizers execute in LIFO order — the last one added runs first.