Skip to main content
Version: 2.x

Response

ZIO HTTP Response is designed to encode HTTP Response. It supports all HTTP status codes and headers along with custom methods and headers (as defined in RFC2616 )

Response Usage

In ZIO HTTP, a Response is used in two contexts, server-side and client-side.

Server Side

In the server-side context, a Response is created and returned by a Handler:

import zio._
import zio.http._

object HelloWorldExample extends ZIOAppDefault {
val routes: Routes[Any, Response] =
Routes(
Method.GET / "text" ->
handler {
Response.text("Hello World!")
},
)

override val run = Server.serve(routes).provide(Server.default)
}

Client Side

In the client-side context, a Response is received from the client after making a request to a server:

import zio._
import zio.http.Header.ContentType.render
import zio.http._

object ClientExample extends ZIOAppDefault {
val program = for {
res <- Client.batched(Request.get("https://zio.dev/"))
contentType <- ZIO.from(res.header(Header.ContentType))
_ <- Console.printLine("------Content Type------")
_ <- Console.printLine(render(contentType))
data <- res.body.asString
_ <- Console.printLine("----------Body----------")
_ <- Console.printLine(data)
} yield ()

override val run = program.provide(Client.default)
}

Creating a Response

A Response can be created with status, headers, and data using the default constructor:

case class Response(
status: Status = Status.Ok,
headers: Headers = Headers.empty,
body: Body = Body.empty,
)

The below snippet creates a response with default params, status as Status.OK, headers as Headers.empty, and data as Body.Empty:

import zio.http._
import zio._

Response()
// res2: Response = Response(
// status = Ok,
// headers = Iterable(),
// body = Body.empty
// )

Status Codes

ZIO HTTP has several constructors for the most common status codes:

MethodDescriptionStatus Code
Response.okSuccessful request200 OK
Response.badRequestThe server cannot or will not process the request due to an apparent client error400 Bad Request
Response.unauthorizedSimilar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided401 Unauthorized
Response.forbiddenThe client does not have access rights to the content; that is, it is unauthorized403 Forbidden
Response.notFoundThe requested resource could not be found but may be available in the future404 Not Found
Response.internalServerErrorA generic error message, given when an unexpected condition was encountered and no more specific message is suitable500 Internal Server Error
Response.serviceUnavailableThe server cannot handle the request (because it is overloaded or down for maintenance)503 Service Unavailable
Response.redirectUsed to inform the client that the resource they're requesting is located at a different URI302 Found (Moved Temporarily)
Response.seeOtherTells the client to look at a different URL for the requested resource303 See Other
Response.gatewayTimeoutThe server was acting as a gateway or proxy and did not receive a timely response from the upstream server504 Gateway Timeout
Response.httpVersionNotSupportedThe server does not support the HTTP protocol version that was used in the request505 HTTP Version Not Supported
Response.networkAuthenticationRequiredThe client needs to authenticate to gain network access511 Network Authentication Required
Response.notExtendedFurther extensions to the request are required for the server to fulfill it510 Not Extended
Response.notImplementedThe server either does not recognize the request method, or it lacks the ability to fulfill the request501 Not Implemented

For example, to create a response with status code 200, we can use Response.ok:

Response.ok
// res3: Response = Response(
// status = Ok,
// headers = Iterable(),
// body = Body.empty
// )

And also to create a response with status code 404, we can use Response.badRequest:

Response.notFound
// res4: Response = Response(
// status = NotFound,
// headers = Iterable(),
// body = Body.empty
// )

Response.notFound("The requested resource could not be found!")
// res5: Response = Response(
// status = NotFound,
// headers = Iterable(),
// body = AsciiStringBody(
// asciiString = The requested resource could not be found!,
// contentType = None
// )
// )

If we want to create a response with a more specific status code, we can use the status method. It takes a Status as a parameter and returns a new Response with the corresponding status code:

Response.status(Status.Continue)

To learn more about status codes, see Status page.

From Plain Text, JSON, and HTML

Response.text creates a response with data as text, content-type header set to text/plain, and status code 200:

Response.text("hey")
// res7: Response = Response(
// status = Ok,
// headers = Iterable(Custom(customName = "content-type", value = "text/plain")),
// body = AsciiStringBody(asciiString = hey, contentType = None)
// )

Response.json creates a response with data as JSON, content-type header set to application/json, and status code 200:

Response.json("""{"greetings": "Hello World!"}""")
// res8: Response = Response(
// status = Ok,
// headers = Iterable(
// Custom(customName = "content-type", value = "application/json")
// ),
// body = AsciiStringBody(
// asciiString = {"greetings": "Hello World!"},
// contentType = None
// )
// )

Response.html creates a response with data as HTML, content-type header set to text/html, and status code 200.

import zio.http.template._

Response.html(Html.fromString("html text"))
// res9: Response = Response(
// status = Ok,
// headers = Iterable(Custom(customName = "content-type", value = "text/html")),
// body = AsciiStringBody(
// asciiString = <!DOCTYPE html>html text,
// contentType = None
// )
// )

Converting Failures to Responses

The Response companion object provides some constructors to convert exceptions into responses. These constructors are useful for error handling by converting failures into appropriate HTTP responses:

Response.fromThrowable Creates a new HTTP response based on the type of throwable provided. This method facilitates the conversion of various types of exceptions into appropriate HTTP responses, making error handling more streamlined:

object Response {
def fromThrowable(throwable: Throwable): Response = ???
}

Here is the table of exceptions and their corresponding status code:

Throwable TypeStatus ClassStatus CodeDescription
AccessDeniedExceptionForbidden403Access to a resource is denied.
IllegalAccessExceptionForbidden403Illegal access to a resource is attempted.
IllegalAccessErrorForbidden403Illegal access to a resource occurs.
NotDirectoryExceptionBadRequest400The specified path is not a directory.
IllegalArgumentExceptionBadRequest400An invalid argument is provided.
java.io.FileNotFoundExceptionNotFound404The specified file or resource is not found.
java.net.ConnectExceptionServiceUnavailable503Unable to connect to a service.
java.net.SocketTimeoutExceptionGatewayTimeout504Connection or read operation timed out.
Others (unrecognized throwable)InternalServerError500An unexpected error occurred.

Another low-level method for error handling is Response.fromCause which creates a response from a Cause:

object Response {
def fromCause(cause: Cause[Any]): Response = ???
}

This constructor is similar to Response.fromThrowable, but it also captures the interruption of the fiber. If the provided Cause is a failure due to interruption, the status code of the response will be Status.RequestTimeout.

We can use Response.fromCause in combination with the Handler#mapErrorCause, Route#handleErrorCause, and Routes#handleErrorCause methods. These methods take a function that maps the Cause[Err] => Err and return a Handler, Route or a Routes with the error handling logic applied:

import zio.http._
import java.io.IOException

val failedHandler = Handler.fail(new IOException())
// failedHandler: Handler[Any, IOException, Any, Nothing] = zio.http.Handler$$anon$13@3182d2cf

failedHandler.mapErrorCause(Response.fromCause)
// res10: Handler[Any, Response, Any, Nothing] = zio.http.Handler$$anon$4@ba6addb
note

In many cases, it is more convenient to use the sandbox method to automatically convert all failures into a corresponding Response. But in some cases, to have more granular control over the error handling, we may want to use Response.fromCause and Response.fromThrowable directly.

info

The Cause is a data structure that represents the result of a failed computation in ZIO. To learn more about Cause, see the Cause page on the ZIO core documentation.

Specialized Response Operators

status to update the status of Response

Response.text("Hello World!").status(Status.NotFound)
// res11: Response = Response(
// status = NotFound,
// headers = Iterable(Custom(customName = "content-type", value = "text/plain")),
// body = AsciiStringBody(asciiString = Hello World!, contentType = None)
// )

updateHeaders to update the headers of Response

Response.ok.updateHeaders(_ => Headers("key", "value"))
// res12: Response = Response(
// status = Ok,
// headers = Iterable(Custom(customName = "key", value = "value")),
// body = Body.empty
// )

Response from HTTP Errors

error creates a response with a provided status code and message.

Response.error(Status.BadRequest, "It's not good!")
// res13: Response = Response(
// status = BadRequest,
// headers = Iterable(),
// body = AsciiStringBody(asciiString = It&#x27;s not good!, contentType = None)
// )

Creating a Response from Server-Sent Events

The Response.fromServerSentEvents method creates a response with a stream of server-sent events:

object Response {
def fromServerSentEvents(stream: ZStream[Any, Nothing, ServerSentEvent[String]]): Response = ???
}

Let's try a complete example:

import zio._
import zio.http._
import zio.stream._
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter.ISO_LOCAL_TIME

object ServerSentExample extends ZIOAppDefault {

val stream: ZStream[Any, Nothing, ServerSentEvent[String]] =
ZStream.repeatWithSchedule(
ServerSentEvent(ISO_LOCAL_TIME.format(LocalDateTime.now)),
Schedule.spaced(1.second),
)

val app =
Routes(
Method.GET / "events" -> handler {
Response.fromServerSentEvents(stream)
},
)
def run = Server.serve(app).provide(Server.default)
}

After running the above example, we can open the browser and navigate to http://localhost:8080/events to see the server-sent events in action. The browser will display the time every second.

Also, we can use the curl command to see the server-sent events:

curl -N http://localhost:8080/events

This will display the time every second in the terminal:

data: 13:51:31.036249

data: 13:51:32.037663

data: 13:51:33.039565

data: 13:51:34.041464

...

Creating a Response from a WebSocketApp

The Response.fromWebSocketApp constructor takes a WebSocketApp and creates a Response with a WebSocket connection:

object Response {
def fromWebSocketApp[R](app: WebSocketApp[R]): ZIO[R, Nothing, Response] = ???
}

Let's try an echo server which sends back the received messages:

import zio._
import zio.http._

object WebsocketExample extends ZIOAppDefault {

val routes: Routes[Any, Response] = {
Routes(
Method.GET / "echo" -> handler {
Response.fromSocketApp(
WebSocketApp(
handler { (channel: WebSocketChannel) =>
channel.receiveAll {
case ChannelEvent.Read(message) =>
channel.send(ChannelEvent.read(message))
case other =>
ZIO.debug(other)
}
},
),
)
},
)
}

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

To test this example, we can use the websocat command-line tool:

> websocat ws://localhost:8080/echo
hello
hello
bye
bye

Operations

Adding Cookies and Flashes to Response

addCookie adds cookies in the headers of the response:

import zio.http._

val cookie = Cookie.Response("key", "value")
Response.ok.addCookie(cookie)

addFlash adds flash messages in the headers of the response:

import zio.http._

val flash = Flash.setValue("key1", "value1")
Response.ok.addFlash(flash)

Working with Headers

There are various methods to work with headers in Response which we have discussed in the Headers page.