HTTP Model
zio-http-model is a pure, zero-dependency HTTP data model for building HTTP clients and servers. It provides immutable types representing requests, responses, headers, URLs, paths, query parameters, and all HTTP primitives.
The module is designed as a pure data layer:
- Zero effects: no streaming, no I/O, no mutable state (except monotonic lazy-parse caches in
Headers) - Zero ZIO dependency: uses
zio.blocks.chunk.Chunk, notzio.Chunk - Single encoding contract:
PathandQueryParamsstore values decoded internally, encode only on output - Cross-platform: JVM and Scala.js support
- Cross-version: Scala 2.13 and 3.x support
package zio.http
// Core request/response types
final case class Request(method: Method, url: URL, headers: Headers, body: Body, version: Version)
final case class Response(status: Status, headers: Headers, body: Body, version: Version)
// URL structure
final case class URL(scheme: Option[Scheme], host: Option[String], port: Option[Int],
path: Path, queryParams: QueryParams, fragment: Option[String])
final case class Path(segments: Chunk[String], hasLeadingSlash: Boolean, trailingSlash: Boolean)
final class QueryParams private[http] (...)
// HTTP primitives
sealed abstract class Method(val name: String, val ordinal: Int)
opaque type Status = Int // Scala 3
sealed abstract class Version(val major: Int, val minor: Int)
sealed trait Scheme
// Headers and body
final class Headers private[http] (...)
sealed trait Header
final class Body private (val data: Chunk[Byte], val contentType: ContentType)
// Supporting types
final case class ContentType(mediaType: MediaType, boundary: Option[Boundary], charset: Option[Charset])
final case class ResponseCookie(...), RequestCookie(name: String, value: String)
final case class Form(entries: Chunk[(String, String)])
Motivation​
HTTP libraries often couple protocol concerns with effects and streaming, making it difficult to:
- Share data structures across client and server implementations
- Serialize requests/responses for caching or testing
- Work with HTTP primitives without committing to a specific effect system
zio-http-model solves this by providing pure data types that:
- Represent all HTTP concepts as immutable values
- Encode only on output (decode on input, store decoded)
- Parse incrementally with lazy caching (
Headersparses typed headers on first access and caches the result) - Work with any effect system or none at all
Installation​
Add the following to your build.sbt:
libraryDependencies += "dev.zio" %% "zio-http-model" % "<version>"
For cross-platform projects (Scala.js):
libraryDependencies += "dev.zio" %%% "zio-http-model" % "<version>"
Supported Scala versions: 2.13.x and 3.x
Quick Start​
Creating a request with query parameters:
import zio.http._
val url = URL.parse("https://api.example.com/users?active=true").toOption.get
val request = Request.get(url)
val withHeader = request.addHeader("authorization", "Bearer token123")
Creating a JSON response:
import zio.http._
val jsonBody = Body.fromString("""{"message":"ok"}""", Charset.UTF8)
val response = Response(
status = Status.Ok,
body = jsonBody,
headers = Headers("content-type" -> "application/json")
)
Method​
Method represents standard HTTP methods as case objects:
sealed abstract class Method(val name: String, val ordinal: Int)
Predefined Methods​
import zio.http.Method
val get = Method.GET
val post = Method.POST
val put = Method.PUT
val delete = Method.DELETE
val patch = Method.PATCH
val head = Method.HEAD
val options = Method.OPTIONS
val trace = Method.TRACE
val connect = Method.CONNECT
Parsing​
import zio.http.Method
Method.fromString("GET") // Some(Method.GET)
Method.fromString("POST") // Some(Method.POST)
Method.fromString("CUSTOM") // None (unknown method)
Rendering​
import zio.http.Method
Method.render(Method.GET) // "GET"
Method.GET.name // "GET"
Method.GET.toString // "GET"
Status​
Status is an opaque type alias for Int in Scala 3 (AnyVal wrapper in Scala 2.13), providing zero-allocation status codes with predefined constants.
opaque type Status = Int // Scala 3
Predefined Status Codes​
Status codes are organized by category:
import zio.http.Status
// 1xx Informational
Status.Continue // 100
Status.SwitchingProtocols // 101
// 2xx Success
Status.Ok // 200
Status.Created // 201
Status.NoContent // 204
// 3xx Redirection
Status.MovedPermanently // 301
Status.Found // 302
Status.SeeOther // 303
Status.NotModified // 304
// 4xx Client Errors
Status.BadRequest // 400
Status.Unauthorized // 401
Status.Forbidden // 403
Status.NotFound // 404
// 5xx Server Errors
Status.InternalServerError // 500
Status.BadGateway // 502
Status.ServiceUnavailable // 503
Creating Status Codes​
import zio.http.Status
val custom = Status(418) // I'm a teapot
val ok = Status.fromInt(200) // Status.Ok
Status Code Operations​
import zio.http.Status
val status = Status.Ok
status.code // 200
status.text // "OK"
status.isSuccess // true (2xx)
status.isInformational // false (1xx)
status.isRedirection // false (3xx)
status.isClientError // false (4xx)
status.isServerError // false (5xx)
status.isError // false (4xx or 5xx)
Version​
Version represents HTTP protocol versions:
sealed abstract class Version(val major: Int, val minor: Int)
Predefined Versions​
import zio.http.Version
val v10 = Version.`HTTP/1.0`
val v11 = Version.`HTTP/1.1`
val v20 = Version.`HTTP/2.0`
val v30 = Version.`HTTP/3.0`
Parsing and Rendering​
import zio.http.Version
Version.fromString("HTTP/1.1") // Some(Version.`HTTP/1.1`)
Version.fromString("HTTP/2") // Some(Version.`HTTP/2.0`)
Version.fromString("HTTP/3") // Some(Version.`HTTP/3.0`)
Version.render(Version.`HTTP/1.1`) // "HTTP/1.1"
Version.`HTTP/2.0`.text // "HTTP/2.0"
Scheme​
Scheme represents URL schemes with support for HTTP, HTTPS, WebSocket (WS, WSS), and custom schemes:
sealed trait Scheme {
def text: String
def defaultPort: Option[Int]
def isSecure: Boolean
def isWebSocket: Boolean
}
Predefined Schemes​
import zio.http.Scheme
val http = Scheme.HTTP // http://, port 80
val https = Scheme.HTTPS // https://, port 443
val ws = Scheme.WS // ws://, port 80
val wss = Scheme.WSS // wss://, port 443
Scheme Properties​
import zio.http.Scheme
Scheme.HTTPS.isSecure // true
Scheme.WS.isWebSocket // true
Scheme.HTTP.defaultPort // Some(80)
Custom Schemes​
import zio.http.Scheme
val custom = Scheme.Custom("git+ssh")
custom.text // "git+ssh"
custom.defaultPort // None
Parsing​
import zio.http.Scheme
Scheme.fromString("https") // Scheme.HTTPS
Scheme.fromString("wss") // Scheme.WSS
Scheme.fromString("custom") // Scheme.Custom("custom")
Charset​
Charset represents character encodings with JVM-only conversion to java.nio.charset.Charset:
sealed abstract class Charset(val name: String)
Predefined Charsets​
import zio.http.Charset
val utf8 = Charset.UTF8 // "UTF-8"
val ascii = Charset.ASCII // "US-ASCII"
val iso88591 = Charset.ISO_8859_1 // "ISO-8859-1"
val utf16 = Charset.UTF16 // "UTF-16"
val utf16be = Charset.UTF16BE // "UTF-16BE"
val utf16le = Charset.UTF16LE // "UTF-16LE"
Parsing​
import zio.http.Charset
Charset.fromString("UTF-8") // Some(Charset.UTF8)
Charset.fromString("utf8") // Some(Charset.UTF8) (case-insensitive)
Charset.fromString("ISO-8859-1") // Some(Charset.ISO_8859_1)
Charset.fromString("LATIN1") // Some(Charset.ISO_8859_1) (alias)
Boundary​
Boundary represents multipart form-data boundaries:
import zio.http.Boundary
val boundary = Boundary("----WebKitFormBoundary7MA4YWxkTrZu0gW")
boundary.value // "----WebKitFormBoundary7MA4YWxkTrZu0gW"
boundary.toString // "----WebKitFormBoundary7MA4YWxkTrZu0gW"
Generating Boundaries​
import zio.http.Boundary
val generated = Boundary.generate // random 24-character alphanumeric string
PercentEncoder​
PercentEncoder provides RFC 3986 percent-encoding for URL components. Each URL component has different encoding rules:
import zio.http.PercentEncoder
import zio.http.PercentEncoder.ComponentType
val segment = PercentEncoder.encode("hello world", ComponentType.PathSegment)
// "hello%20world"
val queryKey = PercentEncoder.encode("filter[name]", ComponentType.QueryKey)
// "filter%5Bname%5D"
val decoded = PercentEncoder.decode("hello%20world")
// "hello world"
Component Types​
The encoder recognizes these component types:
PathSegment: path segments between/QueryKey: query parameter namesQueryValue: query parameter valuesFragment: fragment identifiers after#UserInfo: userinfo in authority
Each type has specific rules for which characters must be percent-encoded.
ContentType​
ContentType combines a media type with optional charset and boundary parameters:
import zio.http.{ContentType, Charset, Boundary}
import zio.blocks.mediatype.MediaTypes
val json = ContentType(MediaTypes.application.`json`)
val htmlUtf8 = ContentType(
mediaType = MediaTypes.text.`html`,
charset = Some(Charset.UTF8)
)
val multipart = ContentType(
mediaType = MediaTypes.multipart.`form-data`,
boundary = Some(Boundary("----boundary"))
)
Parsing​
import zio.http.ContentType
ContentType.parse("application/json")
// Right(ContentType(MediaType("application", "json")))
ContentType.parse("text/html; charset=utf-8")
// Right(ContentType(..., charset = Some(Charset.UTF8)))
ContentType.parse("multipart/form-data; boundary=abc123")
// Right(ContentType(..., boundary = Some(Boundary("abc123"))))
ContentType.parse("")
// Left("Invalid content type: cannot be empty")
Rendering​
import zio.http.{ContentType, Charset}
import zio.blocks.mediatype.MediaTypes
val ct = ContentType(
MediaTypes.text.`plain`,
charset = Some(Charset.UTF8)
)
ct.render // "text/plain; charset=UTF-8"
Predefined Content Types​
import zio.http.ContentType
val json = ContentType.`application/json`
val plain = ContentType.`text/plain`
val html = ContentType.`text/html`
val binary = ContentType.`application/octet-stream`
Path​
Path represents URL paths with decoded segments stored internally:
final case class Path(
segments: Chunk[String],
hasLeadingSlash: Boolean,
trailingSlash: Boolean
)
Paths use a single encoding contract: decode on input, store decoded, encode on output.
Creating Paths​
import zio.http.Path
val empty = Path.empty // ""
val root = Path.root // "/"
val users = Path("/users") // segments: ["users"], leading slash: true
val api = Path("api/v1/users/") // segments: ["api", "v1", "users"], trailing slash: true
Parsing Encoded Paths​
Path.fromEncoded decodes percent-encoded segments:
import zio.http.Path
val path = Path.fromEncoded("/hello%20world/foo%2Fbar")
// Path(Chunk("hello world", "foo/bar"), hasLeadingSlash = true, trailingSlash = false)
path.segments(0) // "hello world" (decoded)
path.segments(1) // "foo/bar" (decoded)
Building Paths​
import zio.http.Path
val base = Path("/api")
val extended = base / "users" / "123" // "/api/users/123"
val combined = Path("/api") ++ Path("v1/users") // "/api/v1/users"
Encoding Paths​
import zio.http.Path
import zio.blocks.chunk.Chunk
val path = Path(Chunk("hello world", "foo/bar"), hasLeadingSlash = true, trailingSlash = false)
path.encode // "/hello%20world/foo%2Fbar" (percent-encoded)
path.render // "/hello world/foo/bar" (decoded for display)
Path Properties​
import zio.http.Path
val path = Path("/api/v1/users/")
path.isEmpty // false
path.nonEmpty // true
path.length // 3 (number of segments)
path.hasLeadingSlash // true
path.trailingSlash // true
Path Navigation​
import zio.http.Path
val path = Path("/api/v1/users")
// Slash manipulation
path.addLeadingSlash // same (already has one)
path.dropLeadingSlash // "api/v1/users" (relative)
path.addTrailingSlash // "/api/v1/users/"
path.dropTrailingSlash // same (already no trailing slash)
// Inspecting
path.isRoot // false (root is "/" with no segments)
Path.root.isRoot // true
// Prefix checking
path.startsWith(Path("api/v1")) // true
// Slicing
path.drop(1) // "/v1/users" (drop first segment)
path.take(2) // "/api/v1" (take first 2 segments)
path.dropRight(1) // "/api/v1" (drop last segment)
path.initial // "/api/v1" (all but last)
path.last // Some("users")
path.reverse // "/users/v1/api"
QueryParams​
QueryParams stores query parameters with multiple values per key:
final class QueryParams private[http] (
private val keys: Array[String],
private val vals: Array[Chunk[String]],
val size: Int
)
Like Path, query parameters store decoded values internally and encode only on output.
Creating QueryParams​
import zio.http.QueryParams
val empty = QueryParams.empty
val params = QueryParams(
"name" -> "Alice",
"age" -> "30",
"active" -> "true"
)
Parsing Encoded Query Strings​
import zio.http.QueryParams
val params = QueryParams.fromEncoded("name=Alice%20Smith&age=30&active=true")
params.getFirst("name") // Some("Alice Smith") (decoded)
params.getFirst("age") // Some("30")
params.getFirst("active") // Some("true")
Accessing Values​
import zio.http.QueryParams
val params = QueryParams(
"color" -> "red",
"color" -> "blue",
"size" -> "large"
)
params.get("color") // Some(Chunk("red", "blue"))
params.getFirst("color") // Some("red")
params.getFirst("size") // Some("large")
params.getFirst("other") // None
params.has("color") // true
Modifying QueryParams​
import zio.http.QueryParams
val params = QueryParams("a" -> "1", "b" -> "2")
val added = params.add("c", "3") // adds "c=3"
val set = params.set("a", "100") // replaces all "a" values
val removed = params.remove("b") // removes all "b" entries
Encoding​
import zio.http.QueryParams
val params = QueryParams(
"name" -> "Alice Smith",
"filter[status]" -> "active"
)
params.encode // "name=Alice%20Smith&filter%5Bstatus%5D=active"
Converting to List​
import zio.http.QueryParams
val params = QueryParams("a" -> "1", "a" -> "2", "b" -> "3")
params.toList // List(("a", "1"), ("a", "2"), ("b", "3"))
URL​
URL combines scheme, host, port, path, query parameters, and fragment:
final case class URL(
scheme: Option[Scheme],
host: Option[String],
port: Option[Int],
path: Path,
queryParams: QueryParams,
fragment: Option[String]
)
Parsing URLs​
import zio.http.URL
val absolute = URL.parse("https://api.example.com:8080/users?active=true#results")
// Right(URL(
// scheme = Some(Scheme.HTTPS),
// host = Some("api.example.com"),
// port = Some(8080),
// path = Path("/users"),
// queryParams = QueryParams("active" -> "true"),
// fragment = Some("results")
// ))
val relative = URL.parse("/api/users?page=2")
// Right(URL(
// scheme = None,
// host = None,
// port = None,
// path = Path("/api/users"),
// queryParams = QueryParams("page" -> "2"),
// fragment = None
// ))
The parser handles:
- IPv6 hosts in brackets:
http://[::1]:8080/ - Userinfo:
https://user:pass@example.com/(userinfo is skipped) - Relative URLs:
/path?query - Fragment identifiers:
#section
Building URLs​
import zio.http.{URL, Path, Scheme}
val base = URL.root // http://localhost/
val extended = base / "api" / "users" // adds path segments
val withQuery = extended ?? ("active", "true") ?? ("page", "1")
// http://localhost/api/users?active=true&page=1
URL from Path​
import zio.http.{URL, Path}
val path = Path("/api/users")
val url = URL.fromPath(path) // relative URL with just path
Encoding URLs​
import zio.http.URL
val url = URL.parse("https://example.com/hello world?name=Alice Smith").toOption.get
url.encode // "https://example.com/hello%20world?name=Alice%20Smith"
url.toString // same as encode
URL Properties​
import zio.http.URL
val absolute = URL.parse("https://example.com/").toOption.get
val relative = URL.parse("/api/users").toOption.get
absolute.isAbsolute // true (has scheme)
relative.isRelative // true (no scheme)
URL Transformation​
import zio.http.{URL, Path, Scheme, QueryParams}
val url = URL.parse("https://api.example.com:8080/users?page=1").toOption.get
// Setting components
url.host("other.com") // changes host
url.port(9090) // changes port
url.scheme(Scheme.HTTP) // changes scheme
url.path(Path("/v2/users")) // replaces path
url.fragment("top") // sets fragment
// Adding paths
url.addPath("123") // appends segment: /users/123
url.addPath(Path("v2/items")) // appends multi-segment path
// Query manipulation
url.addQueryParams(QueryParams("sort" -> "name"))
url.updateQueryParams(_.remove("page"))
// Slash manipulation (delegates to path)
url.addLeadingSlash
url.dropTrailingSlash
// Derived properties
url.hostPort // Some("api.example.com:8080")
url.relative // strips scheme/host/port, keeps path and query
Header​
Header is a trait representing typed HTTP headers:
trait Header {
def headerName: String
def renderedValue: String
}
Each header type has a companion object implementing Header.Typed[H] for parsing and rendering.
Predefined Header Types​
import zio.http.{Header => _, *}
import zio.http.headers
val contentType = headers.ContentType
val accept = headers.Accept
val authorization = headers.Authorization
val host = headers.Host
val userAgent = headers.UserAgent
val cacheControl = headers.CacheControl
val contentLength = headers.ContentLength
val location = headers.Location
val setCookie = headers.SetCookieHeader
val cookie = headers.CookieHeader
Creating Typed Headers​
import zio.http.{Header => _, ContentType, Charset, *}
import zio.http.headers
import zio.blocks.mediatype.MediaTypes
val ct = headers.ContentType(
ContentType(MediaTypes.application.`json`, charset = Some(Charset.UTF8))
)
val auth = headers.Authorization.Bearer("token123")
val host = headers.Host("api.example.com", Some(8080))
Parsing Headers​
import zio.http.{Header => _, *}
import zio.http.headers
headers.ContentType.parse("application/json; charset=utf-8")
// Right(headers.ContentType(...))
headers.Host.parse("example.com:443")
// Right(headers.Host("example.com", Some(443)))
headers.ContentLength.parse("1024")
// Right(headers.ContentLength(1024))
headers.ContentLength.parse("-1")
// Left("Invalid content-length: -1")
Custom Headers​
import zio.http.Header
val custom = Header.Custom("x-request-id", "abc-123")
custom.headerName // "x-request-id"
custom.renderedValue // "abc-123"
Headers​
Headers is a flat array-based collection with lazy monotonic parsing:
final class Headers private[http] (
private val names: Array[String],
private val rawValues: Array[String],
private val parsed: Array[AnyRef], // null -> unparsed, value -> cached
val size: Int
)
When you call get[H], the header is parsed once and cached. Subsequent calls return the cached result.
Creating Headers​
import zio.http.Headers
val empty = Headers.empty
val headers = Headers(
"content-type" -> "application/json",
"authorization" -> "Bearer token",
"x-request-id" -> "abc-123"
)
Getting Typed Headers​
import zio.http.{Headers, *}
import zio.http.{headers => h}
val headers = Headers(
"content-type" -> "application/json",
"content-length" -> "1024"
)
val ct = headers.get(h.ContentType)
// Some(h.ContentType(...)) (parsed and cached)
val cl = headers.get(h.ContentLength)
// Some(h.ContentLength(1024)) (parsed and cached)
val auth = headers.get(h.Authorization)
// None (not present)
Getting Raw Values​
import zio.http.Headers
val headers = Headers("x-custom" -> "value")
headers.rawGet("x-custom") // Some("value")
headers.rawGet("missing") // None
Getting All Headers of a Type​
Some headers can appear multiple times (like Set-Cookie):
import zio.http.{Headers, *}
import zio.http.{headers => h}
val headers = Headers(
"set-cookie" -> "session=abc",
"set-cookie" -> "preference=dark"
)
val cookies = headers.getAll(h.SetCookieHeader)
// Chunk(h.SetCookieHeader(...), h.SetCookieHeader(...))
Modifying Headers​
import zio.http.Headers
val headers = Headers("a" -> "1", "b" -> "2")
val added = headers.add("c", "3") // adds "c: 3"
val set = headers.set("a", "100") // replaces all "a" values
val removed = headers.remove("b") // removes all "b" entries
val has = headers.has("a") // true
Converting to List​
import zio.http.Headers
val headers = Headers("a" -> "1", "b" -> "2")
headers.toList // List(("a", "1"), ("b", "2"))
Combining Headers​
import zio.http.Headers
val auth = Headers("authorization" -> "Bearer token")
val cors = Headers("access-control-allow-origin" -> "*")
val combined = auth ++ cors // Headers(authorization: ..., access-control-allow-origin: ...)
combined.contains("authorization") // true (alias for `has`)
combined.toChunk // Chunk(("authorization", ...), ("access-control-allow-origin", ...))
Body​
Body wraps a materialized Chunk[Byte] with a content type:
final class Body private (
val data: Chunk[Byte],
val contentType: ContentType
)
Creating Bodies​
Body.empty provides an empty body with default application/octet-stream content type:
import zio.http.Body
val empty = Body.empty
// Body(data = Chunk.empty, contentType = application/octet-stream)
Body.fromString creates a body with text/plain content type:
import zio.http.{Body, Charset}
val fromString = Body.fromString("Hello, World!", Charset.UTF8)
// Content-Type: text/plain; charset=UTF-8
Body.fromArray creates a body with default application/octet-stream content type:
import zio.http.Body
val fromBytes = Body.fromArray(Array[Byte](1, 2, 3))
// Content-Type: application/octet-stream
Body.fromChunk creates a body from a Chunk[Byte] with optional content type:
import zio.http.{Body, ContentType}
import zio.blocks.chunk.Chunk
import zio.blocks.mediatype.MediaTypes
val chunk = Chunk[Byte](1, 2, 3, 4, 5)
val body = Body.fromChunk(chunk)
// Content-Type: application/octet-stream (default)
val jsonBody = Body.fromChunk(chunk, ContentType(MediaTypes.application.`json`))
// Content-Type: application/json
Reading Bodies​
Body provides direct access to data and content type:
import zio.http.{Body, Charset}
val body = Body.fromString("Hello!", Charset.UTF8)
body.length // 6
body.isEmpty // false
body.nonEmpty // true
body.asString() // "Hello!" (UTF-8 default)
body.asString(Charset.ASCII) // "Hello!" (explicit charset)
body.data // Chunk[Byte](72, 101, 108, 108, 111, 33)
body.contentType // ContentType(text/plain; charset=UTF-8)
Cookie​
Cookies are split into RequestCookie and ResponseCookie with different structures:
final case class RequestCookie(name: String, value: String)
final case class ResponseCookie(
name: String,
value: String,
domain: Option[String],
path: Option[Path],
maxAge: Option[Long],
isSecure: Boolean,
isHttpOnly: Boolean,
sameSite: Option[SameSite]
)
SameSite​
import zio.http.SameSite
val strict = SameSite.Strict
val lax = SameSite.Lax
val none = SameSite.None_ // underscore avoids conflict with scala.None
Parsing Request Cookies​
import zio.http.Cookie
val cookies = Cookie.parseRequest("session=abc123; preference=dark")
// Chunk(RequestCookie("session", "abc123"), RequestCookie("preference", "dark"))
Parsing Response Cookies​
import zio.http.Cookie
val cookie = Cookie.parseResponse("session=abc; Domain=example.com; Path=/; Secure; HttpOnly; SameSite=Strict")
// Right(ResponseCookie(
// name = "session",
// value = "abc",
// domain = Some("example.com"),
// path = Some(Path("/")),
// isSecure = true,
// isHttpOnly = true,
// sameSite = Some(SameSite.Strict)
// ))
Rendering Cookies​
import zio.http.{Cookie, RequestCookie, ResponseCookie, SameSite, Path}
import zio.blocks.chunk.Chunk
val requestCookies = Chunk(
RequestCookie("session", "abc"),
RequestCookie("theme", "dark")
)
Cookie.renderRequest(requestCookies)
// "session=abc; theme=dark"
val responseCookie = ResponseCookie(
name = "session",
value = "xyz",
domain = Some("example.com"),
path = Some(Path("/")),
maxAge = Some(3600),
isSecure = true,
isHttpOnly = true,
sameSite = Some(SameSite.Strict)
)
Cookie.renderResponse(responseCookie)
// "session=xyz; Domain=example.com; Path=/; Max-Age=3600; Secure; HttpOnly; SameSite=Strict"
Form​
Form represents URL-encoded form data:
final case class Form(entries: Chunk[(String, String)])
Creating Forms​
import zio.http.Form
val empty = Form.empty
val form = Form(
"username" -> "alice",
"password" -> "secret",
"remember" -> "true"
)
Accessing Form Data​
import zio.http.Form
val form = Form(
"tag" -> "scala",
"tag" -> "functional",
"page" -> "1"
)
form.get("tag") // Some("scala") (first value)
form.getAll("tag") // Chunk("scala", "functional")
form.get("page") // Some("1")
form.get("missing") // None
Modifying Forms​
import zio.http.Form
val form = Form("a" -> "1")
val added = form.add("b", "2") // Form(("a", "1"), ("b", "2"))
Encoding and Parsing​
import zio.http.Form
val form = Form(
"name" -> "Alice Smith",
"email" -> "alice@example.com"
)
val encoded = form.encode
// "name=Alice%20Smith&email=alice%40example.com"
val parsed = Form.fromString(encoded)
// Form with decoded entries
FormField​
FormField is a sealed trait for multipart form fields, supporting simple key-value pairs, text parts with optional metadata, and binary parts:
import zio.http._
import zio.blocks.chunk.Chunk
import zio.blocks.mediatype.MediaTypes
// Simple key-value field
val simple = FormField.Simple("username", "alice")
// Text field with optional content type and filename
val text = FormField.Text(
name = "bio",
value = "Hello world",
contentType = Some(ContentType(MediaTypes.text.`plain`)),
filename = None
)
// Binary field with content type and optional filename
val binary = FormField.Binary(
name = "avatar",
data = Chunk.fromArray(Array[Byte](1, 2, 3)),
contentType = ContentType(MediaTypes.image.`png`),
filename = Some("avatar.png")
)
// All variants share a common `name` accessor
simple.name // "username"
text.name // "bio"
binary.name // "avatar"
Request​
Request combines all HTTP request components:
final case class Request(
method: Method,
url: URL,
headers: Headers,
body: Body,
version: Version
)
Creating Requests​
import zio.http._
val url = URL.parse("https://api.example.com/users").toOption.get
val getRequest = Request.get(url)
val jsonBody = Body.fromString("""{"name":"Alice"}""", Charset.UTF8)
val postRequest = Request.post(url, jsonBody)
Request Factory Methods​
import zio.http._
val url = URL.parse("https://api.example.com/resource/1").toOption.get
val body = Body.fromString("""{"name":"updated"}""")
Request.get(url) // GET with empty body
Request.post(url, body) // POST with body
Request.put(url, body) // PUT with body
Request.patch(url, body) // PATCH with body
Request.delete(url) // DELETE with empty body
Request.head(url) // HEAD with empty body
Request.options(url) // OPTIONS with empty body
Modifying Requests​
All modification methods return a new Request—the original is unchanged:
import zio.http._
val request = Request.get(URL.parse("https://api.example.com/users").toOption.get)
// Adding/modifying headers
request.addHeader("Accept", "application/json")
request.addHeaders(Headers("X-A" -> "1", "X-B" -> "2"))
request.setHeader("Accept", "text/html") // replaces existing
request.removeHeader("Accept")
// Replacing components
request.body(Body.fromString("data"))
request.url(URL.parse("/other").toOption.get)
request.method(Method.POST)
request.version(Version.`HTTP/2.0`)
// Functional updates
request.updateHeaders(_.add("X-Custom", "value"))
request.updateUrl(_ / "123") // appends path segment
Full control:
import zio.http._
val url = URL.parse("https://api.example.com/users").toOption.get
val body = Body.fromString("""{"name":"Alice"}""", Charset.UTF8)
val request = Request(
method = Method.POST,
url = url,
headers = Headers(
"content-type" -> "application/json",
"authorization" -> "Bearer token123"
),
body = body,
version = Version.`HTTP/1.1`
)
Accessing Request Data​
import zio.http._
import zio.http.{headers => h}
val request = Request.get(URL.parse("/api/users?page=1").toOption.get)
request.path // Path("/api/users")
request.queryParams // QueryParams("page" -> "1")
request.contentType // Option[ContentType]
request.header(h.Authorization) // Option[h.Authorization]
Response​
Response represents HTTP responses:
final case class Response(
status: Status,
headers: Headers,
body: Body,
version: Version
)
Creating Responses​
import zio.http._
val ok = Response.ok // 200 OK, empty body
val notFound = Response.notFound // 404 Not Found, empty body
Response Factory Methods​
import zio.http._
// Status-only responses
Response.ok // 200
Response.notFound // 404
Response.badRequest // 400
Response.unauthorized // 401
Response.forbidden // 403
Response.internalServerError // 500
Response.serviceUnavailable // 503
// Responses with bodies
Response.text("Hello, World!") // 200 with text/plain body
Response.json("""{'ok':true}""") // 200 with application/json in headers and body
// Redirects
Response.redirect("/new-location") // 307 Temporary Redirect
Response.redirect("/new-location", isPermanent = true) // 308 Permanent Redirect
Response.seeOther("/other") // 303 See Other
Note that Response.json creates bodies with application/json content-type on the Body itself, not just in the headers.
Modifying Responses​
import zio.http._
val response = Response.ok
// Adding/modifying headers
response.addHeader("X-Custom", "value")
response.setHeader("X-Custom", "new-value")
response.removeHeader("X-Custom")
// Replacing components
response.body(Body.fromString("data"))
response.status(Status.Created)
response.version(Version.`HTTP/2.0`)
// Functional update
response.updateHeaders(_.add("X-Request-Id", "abc"))
// Add a Set-Cookie header
response.addCookie(ResponseCookie("session", "abc123"))
Full control:
import zio.http._
val jsonBody = Body.fromString("""{"message":"created"}""", Charset.UTF8)
val response = Response(
status = Status.Created,
headers = Headers(
"content-type" -> "application/json",
"location" -> "/users/123"
),
body = jsonBody,
version = Version.`HTTP/1.1`
)
Accessing Response Data​
import zio.http._
import zio.http.{headers => h}
val response = Response.ok
response.status.code // 200
response.status.isSuccess // true
response.contentType // Option[ContentType]
response.header(h.ContentType) // Option[h.ContentType]
response.header(h.Location) // Option[h.Location]
Advanced Usage​
Building a Complete HTTP Exchange​
import zio.http._
// Build request
val url = URL.parse("https://api.example.com/users").toOption.get
val requestBody = Body.fromString("""{"name":"Alice","age":30}""", Charset.UTF8)
val request = Request(
method = Method.POST,
url = url,
headers = Headers(
"content-type" -> "application/json",
"authorization" -> "Bearer abc123",
"user-agent" -> "MyClient/1.0"
),
body = requestBody,
version = Version.`HTTP/1.1`
)
// Build response
val responseBody = Body.fromString("""{"id":123,"name":"Alice","age":30}""", Charset.UTF8)
val response = Response(
status = Status.Created,
headers = Headers(
"content-type" -> "application/json",
"location" -> "/users/123"
),
body = responseBody,
version = Version.`HTTP/1.1`
)
URL Building with Fluent API​
import zio.http._
val url = URL.parse("https://api.example.com").toOption.get
val extended = (url / "v1" / "users" / "123") ?? ("include", "profile") ?? ("include", "posts")
extended.encode
// "https://api.example.com/v1/users/123?include=profile&include=posts"
Typed Header Access​
import zio.http._
import zio.http.{headers => h}
val headers = Headers(
"content-type" -> "application/json; charset=utf-8",
"content-length" -> "1024",
"authorization" -> "Bearer token"
)
// Type-safe header access with parsing
val ct = headers.get(h.ContentType)
ct.map(_.value.charset) // Some(Some(Charset.UTF8))
val cl = headers.get(h.ContentLength)
cl.map(_.length) // Some(1024)
// Raw access
headers.rawGet("authorization") // Some("Bearer token")
Cookie Management​
import zio.http._
import zio.blocks.chunk.Chunk
// Parse cookies from request header
val cookieHeader = "session=abc; theme=dark"
val requestCookies = Cookie.parseRequest(cookieHeader)
// Create response with Set-Cookie headers
val sessionCookie = ResponseCookie(
name = "session",
value = "xyz123",
path = Some(Path("/")),
maxAge = Some(3600),
isSecure = true,
isHttpOnly = true,
sameSite = Some(SameSite.Strict)
)
val response = Response(
status = Status.Ok,
headers = Headers(
"set-cookie" -> Cookie.renderResponse(sessionCookie)
),
body = Body.empty
)
Form Submission​
import zio.http._
val form = Form(
"username" -> "alice",
"password" -> "secret",
"remember" -> "true"
)
val formBody = Body.fromString(form.encode, Charset.UTF8)
val request = Request(
method = Method.POST,
url = URL.parse("/login").toOption.get,
headers = Headers(
"content-type" -> "application/x-www-form-urlencoded"
),
body = formBody,
version = Version.`HTTP/1.1`
)
Design Principles​
Single Encoding Contract​
Path and QueryParams store decoded values internally. Encoding happens only at output boundaries:
Path.fromEncoded(s)decodes, stores decoded segmentsPath.encodeencodes segments for transmissionQueryParams.fromEncoded(s)decodes, stores decoded key-value pairsQueryParams.encodeencodes for transmission
This eliminates double-encoding bugs and clarifies responsibilities.
Lazy Header Parsing​
Headers stores raw string values and parses typed headers on first access. Parsed results are cached in a parallel Array[AnyRef] for O(1) subsequent lookups. This design:
- Avoids parsing headers that are never accessed
- Avoids re-parsing the same header multiple times
- Supports unknown/custom headers without parsing failures
No Streaming​
Bodies are fully materialized Chunk[Byte]. Streaming is left to higher-level HTTP libraries that compose with this data model. This keeps the model simple and effect-free.
Zero ZIO Dependency​
The module uses zio.blocks.chunk.Chunk instead of zio.Chunk, making it usable in any Scala project without ZIO.
Schema-Based Typed Access (zio-http-model-schema)​
The zio-http-model-schema module provides schema-based extraction of query parameters and headers with automatic decoding and validation.
Installation​
Add the following to your build.sbt:
libraryDependencies += "dev.zio" %% "zio-blocks-http-model-schema" % "<version>"
For cross-platform projects (Scala.js):
libraryDependencies += "dev.zio" %%% "zio-blocks-http-model-schema" % "<version>"
Imports​
Import the schema module to enable extension methods:
import zio.http.schema._
import zio.blocks.schema.Schema
Query Parameter Extraction​
QueryParams gains schema-based extraction methods via implicit conversions:
import zio.http.{QueryParams, URL}
import zio.http.schema._
import zio.blocks.schema.Schema
val url = URL.parse("/api/users?page=2&tag=scala&tag=fp").toOption.get
val params = url.queryParams
// Extract single value with automatic decoding
params.query[Int]("page")
// Right(2)
// Extract all values for a key
params.queryAll[String]("tag")
// Right(Chunk("scala", "fp"))
// Extract with default fallback
params.queryOrElse[Int]("limit", 10)
// 10 - uses default since "limit" not present
Header Extraction​
Headers gains schema-based extraction methods:
import zio.http.{Headers, Request, URL}
import zio.http.schema._
import zio.blocks.schema.Schema
val request = Request.get(URL.parse("/").toOption.get)
.addHeader("x-page", "5")
.addHeader("x-tag", "scala")
.addHeader("x-tag", "functional")
val headers = request.headers
// Extract single header value
headers.header[Int]("x-page")
// Right(5)
// Extract all header values
headers.headerAll[String]("x-tag")
// Right(Chunk("scala", "functional"))
// Extract with default fallback
headers.headerOrElse[Int]("x-limit", 100)
// 100
Request and Response Extensions​
Request and Response gain schema-based extraction methods that delegate to their query parameters and headers:
import zio.http.{Request, Response, URL}
import zio.http.schema._
import zio.blocks.schema.Schema
// Request query parameter extraction
val request = Request.get(URL.parse("/search?q=zio&limit=20").toOption.get)
request.query[String]("q")
// Right("zio")
request.query[Int]("limit")
// Right(20)
// Response header extraction via schema
val response = Response.ok.addHeader("x-correlation-id", "abc-123")
val responseOps = new ResponseSchemaOps(response)
responseOps.header[String]("x-correlation-id")
Error Handling​
Schema-based extraction returns Either[QueryParamError, A] or Either[HeaderError, A] for explicit error handling:
import zio.http.{QueryParams, URL}
import zio.http.schema._
import zio.blocks.schema.Schema
val params = QueryParams("name" -> "Alice", "age" -> "invalid")
params.query[String]("name") match {
case Right(name) => println(s"Name: $name")
case Left(QueryParamError.Missing(key)) => println(s"Missing key: $key")
case Left(QueryParamError.Malformed(key, value, cause)) =>
println(s"Failed to parse $key=$value: $cause")
}
params.query[Int]("age") match {
case Right(age) => println(s"Age: $age")
case Left(QueryParamError.Missing(key)) => println(s"Missing key: $key")
case Left(QueryParamError.Malformed(key, value, cause)) =>
println(s"Failed to parse $key=$value: $cause")
}
Supported Types​
The schema module provides built-in Schema instances for common types. Any type with a Schema[T] can be extracted:
- Primitives:
String,Int,Long,Boolean,Double,Float,Short,Byte,Char - Big Numbers:
BigInt,BigDecimal - UUID:
java.util.UUID
For custom types, define a Schema[T] instance using schema derivation or manual construction.