Skip to main content
Version: 2.x

Request

ZIO HTTP Request is designed in the simplest way possible to decode an HTTP Request into a ZIO HTTP request. It supports all HTTP request methods (as defined in RFC2616 ) and headers along with custom methods and headers.

Accessing Incoming Request​

To access the incoming request, we can use a Handler which takes a Request as input and returns a Response:

import zio._
import zio.http._

Routes(
Method.POST / "echo" ->
handler { (req: Request) =>
req.body.asString(Charsets.Utf8).map(Response.text(_)).sandbox
}
)

To learn more about handlers, please refer to the Handler section.

Creating a Request​

The default constructor of Request takes the following parameters as input: version, method, url, headers, body, remoteAddress:

final case class Request(
version: Version = Version.Default,
method: Method = Method.ANY,
url: URL = URL.empty,
headers: Headers = Headers.empty,
body: Body = Body.empty,
remoteAddress: Option[InetAddress] = None,
) extends HeaderOps[Request]

The below snippet creates a request with default params, headers as Headers.empty, data as Body.Empty, remoteAddress as None:

import zio.http._

Request(method = Method.GET, url = URL(Path.root))
// res1: Request = Request(
// version = Default,
// method = GET,
// url = URL(
// path = Path(flags = 3, segments = IndexedSeq()),
// kind = Relative,
// queryParams = JavaLinkedHashMapQueryParams(underlying = {}),
// fragment = None
// ),
// headers = Iterable(),
// body = Body.empty,
// remoteAddress = None,
// remoteCertificate = None
// )

There are also some helper methods to create requests for different HTTP methods inside the Request's companion object: delete, get, head, options, patch, post, and put.

We can access the request's details using the below fields:

  • method to access request method
  • headers to get all the headers in the Request
  • body to access the content of the request as a Body
  • url to access request URL
  • remoteAddress to access the request's remote address if available
  • version to access the HTTP version
note

Please note that usually, we don't create requests on the server-side. Creating requests is useful while writing unit tests or when we call other services using the ZIO HTTP Client.

Request with Query Params​

Query params can be added in the request using url in Request, URL stores query params as Map[String, List[String]].

The below snippet creates a request with query params: ?q=a&q=b&q=c

import zio._
import zio.http._

Request.get(url = URL(Path.root, queryParams = QueryParams("q" -> Chunk("a","b","c"))))

The Request#url.queryParams can be used to read query params from the request.

Operations​

Leading/Trailing Slash​

The Request class provides the following methods to add or drop leading/trailing slashes from the URL:

  • addLeadingSlash
  • addTrailingSlash
  • dropLeadingSlash
  • dropTrailingSlash

Patching Requests​

To patch a request, we can use the patch method, which takes a Request.Patch as input:

import zio._
import zio.http._

Request
.get("http://localhost:8080/users")
.patch(
Request.Patch(
addHeaders = Headers(Header.ContentType(MediaType.application.`json`)),
addQueryParams = QueryParams("role" -> Chunk("reviewer", "editor"))
)
)
// res3: Request = Request(
// version = Default,
// method = GET,
// url = URL(
// path = Path(flags = 1, segments = IndexedSeq("users")),
// kind = Absolute(
// scheme = HTTP,
// host = "localhost",
// originalPort = Some(value = 8080)
// ),
// queryParams = JavaLinkedHashMapQueryParams(
// underlying = {role=[reviewer, editor]}
// ),
// fragment = None
// ),
// headers = Iterable(
// ContentType(
// mediaType = MediaType(
// mainType = "application",
// subType = "json",
// compressible = true,
// binary = false,
// fileExtensions = List("json", "map"),
// extensions = Map(),
// parameters = Map()
// ),
// boundary = None,
// charset = None
// )
// ),
// body = Body.empty,
// remoteAddress = None,
// remoteCertificate = None
// )

Request Headers​

There are several methods available to get, update, and remove headers from a Request:

  1. To access headers, we can use the following methods:

    • Request#header to get a single header
    • Request#headerOrFail to get a single header or fail if it doesn't exist
    • Request#headers to get all headers
    • Request#rawHeader to get a single header as a string
  2. To update headers, the Request#updateHeaders takes a Headers => Headers function as input and returns a new Request with updated headers.

  3. To add headers, the Request#addHeader and Request#addHeaders methods are available.

  4. To remove headers, the Request#removeHeader and Request#removeHeaders methods are available.

  5. To set headers, the Request#setHeaders method is available.

Request Body​

There are several methods available to get, update, and remove body from a Request.

  • The Request#body accesses the body of the request.
  • The Request#withBody takes a Body as input and returns a new Request with the updated body.
  • The Request#updateBody and Request#updateBody a Body => Body or Body => ZIO[R, E, Body] function as input and returns a new Request with the updated body.
  • The Request#collect collects the streaming body of the request and returns a new Request with the collected body.
  • The Request#ignoreBody consumes the streaming body fully and returns a new Request with an empty body.

Retrieving Query Parameters​

There are several methods available to access query parameters from a Request.

To get a single query parameter, we can use the Request#queryParam method that takes a String as the input key and returns an Option[String]:

// curl -X GET https://localhost:8080/search?q=value -i
import zio._
import zio.http._

object QueryParamExample extends ZIOAppDefault {

val app =
Routes(
Method.GET / "search" -> handler { (req: Request) =>
val queries = req.queryParam("q")
queries match {
case Some(value) =>
Response.text(s"Value of query param q is $value")
case None =>
Response.badRequest(s"The q query parameter is missing!")
}
},
)

def run = Server.serve(app).provide(Server.default)
}

The typed version of Request#queryParam is Request#queryParamTo which takes a key and a type parameter of type T and finally returns a Either[QueryParamsError, T] value:

// curl -X GET https://localhost:8080/search?age=42 -i
import zio.http._
object TypedQueryParamExample extends ZIOAppDefault {
val app =
Routes(
Method.GET / "search" -> Handler.fromFunctionHandler { (req: Request) =>
val response: ZIO[Any, QueryParamsError, Response] =
ZIO.fromEither(req.queryParamTo[Int]("age"))
.map(value => Response.text(s"The value of age query param is: $value"))

Handler.fromZIO(response).catchAll {
case QueryParamsError.Missing(name) =>
Handler.badRequest(s"The $name query param is missing")
case QueryParamsError.Malformed(name, codec, values) =>
Handler.badRequest(s"The value of $name query param is malformed")
}
},
)

def run = Server.serve(app).provide(Server.default)
}
info

In the above example, instead of using ZIO.fromEither(req.queryParamTo[Int]("age")) we can use req.queryParamToZIO[Int]("age") to get a ZIO value directly which encodes the error type in the ZIO effect.

To retrieve all query parameter values for a key, we can use the Request#queryParams method that takes a String as the input key and returns a Chunk[String]:

// curl -X GET https://localhost:8080/search?q=value1&q=value2 -i

import zio._
import zio.http._

object QueryParamsExample extends ZIOAppDefault {
val app =
Routes(
Method.GET / "search" -> handler { (req: Request) =>
val queries = req.queryParams("q")
if (queries.nonEmpty) {
val text = queries.mkString("Here is the list of values for the q query param: [", ",", "]")
Response.text(text)
} else {
Response.badRequest(s"The q query parameter is missing!")
}
},
)

def run = Server.serve(app).provide(Server.default)
}

The typed version of Request#queryParams is Request#queryParamsTo which takes a key and a type parameter of type T and finally returns a Either[QueryParamsError, Chunk[T]] value.

note

All the above methods also have OrElse versions which take a default value as input and return the default value if the query parameter is not found, e.g. Request#queryParamOrElse, Request#queryParamToOrElse, Request#queryParamsOrElse, Request#queryParamsToOrElse.

Using the Request#queryParameters method, we can access the query parameters of the request which returns a QueryParams object.

Modifying Query Parameters​

When we are working with ZIO HTTP Client, we need to create a new Request and may need to set/update/remove query parameters. In such cases, we have the following methods available: addQueryParam, addQueryParams, removeQueryParam, removeQueryParams, setQueryParams, and updateQueryParams.

import zio._
import zio.http._

object QueryParamClientExample extends ZIOAppDefault {
def run =
Client.batched(
Request
.get("http://localhost:8080/search")
.addQueryParam("language", "scala")
.addQueryParam("q", "How to Write HTTP App")
.addQueryParams("tag", Chunk("zio", "http", "scala")),
).provide(Client.default)
}

The above example sends a GET request to http://localhost:8080/search?language=scala&q=How+to+Write+HTTP+App&tag=zio&tag=http&tag=scala.

Retrieving URL/Path​

To access the URL of the request, we can utilize the Request#url method, which yields a URL object. For updating the URL of the request, we can use the Request#updateURL method, which takes a URL => URL function as input. This function allows us to update the URL and return a new Request object with the updated URL.

If we want to access the path of the request, we can use the Request#path method which returns a Path object. Also, we can use the Request#path method which takes a Path and returns a new Request with the updated path.

Retrieving Cookies and Flashes​

Cookies and Flashes

To access all cookies in the request, we can use the Request#cookies method which returns a Chunk[Cookie]:

val cookies = request.cookies
// cookies: Chunk[Cookie] = IndexedSeq(
// Request(name = "key1", content = "value1"),
// Request(name = "key2", content = "value2")
// )

To access a single cookie, we can use the Request#cookie method which takes the name of the cookie as input and returns an Option[Cookie].

val cookie = request.cookie("key1")
// cookie: Option[Cookie] = Some(
// value = Request(name = "key1", content = "value1")
// )

To encode errors in the ZIO effect when a cookie is not found, we can use the Request#cookieWithOrFail method which takes three groups of parameters: name of the cookie, error message, and finally a function that takes a cookie and returns a ZIO effect:

trait Request {
def cookieWithOrFail[R, E, A](name: String)(missingCookieError: E)(f: Cookie => ZIO[R, E, A]): ZIO[R, E, A]
}

Here is an example of using Request#cookieWithOrFail:

case class CookieNotFound(cookie: String)

val key = "key3"
val effect: ZIO[Any, CookieNotFound, Cookie] =
request.cookieWithOrFail(key)(CookieNotFound(key))(c => ZIO.succeed(c))

Or simply use the Request#cookieWithZIO method which does the same but Throwable is used as the error type:

val effect: ZIO[Any, Throwable, Cookie] = 
request.cookieWithZIO("key3")(c => ZIO.succeed(c))

To get a flash message of type A with the given key, we can use the Request#flash method which takes a Flash[A] as input and returns an Option[A]:

val flashValue = request.flash(Flash.get[Int]("key1"))

Client-side Example​

In the below example, we are creating a Request using the Request.get method and then calling the Client.batched method to send the request to the servers:

import zio._
import zio.http._

object ClientExample extends ZIOAppDefault {
def run = Client
.batched(Request.get("http://localhost:8080/users/2"))
.flatMap(_.body.asString)
.debug("Response Body: ")
.provide(Client.default)

}