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:
Method | Description | Status Code |
---|---|---|
Response.ok | Successful request | 200 OK |
Response.badRequest | The server cannot or will not process the request due to an apparent client error | 400 Bad Request |
Response.unauthorized | Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided | 401 Unauthorized |
Response.forbidden | The client does not have access rights to the content; that is, it is unauthorized | 403 Forbidden |
Response.notFound | The requested resource could not be found but may be available in the future | 404 Not Found |
Response.internalServerError | A generic error message, given when an unexpected condition was encountered and no more specific message is suitable | 500 Internal Server Error |
Response.serviceUnavailable | The server cannot handle the request (because it is overloaded or down for maintenance) | 503 Service Unavailable |
Response.redirect | Used to inform the client that the resource they're requesting is located at a different URI | 302 Found (Moved Temporarily) |
Response.seeOther | Tells the client to look at a different URL for the requested resource | 303 See Other |
Response.gatewayTimeout | The server was acting as a gateway or proxy and did not receive a timely response from the upstream server | 504 Gateway Timeout |
Response.httpVersionNotSupported | The server does not support the HTTP protocol version that was used in the request | 505 HTTP Version Not Supported |
Response.networkAuthenticationRequired | The client needs to authenticate to gain network access | 511 Network Authentication Required |
Response.notExtended | Further extensions to the request are required for the server to fulfill it | 510 Not Extended |
Response.notImplemented | The server either does not recognize the request method, or it lacks the ability to fulfill the request | 501 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 Type | Status Class | Status Code | Description |
---|---|---|---|
AccessDeniedException | Forbidden | 403 | Access to a resource is denied. |
IllegalAccessException | Forbidden | 403 | Illegal access to a resource is attempted. |
IllegalAccessError | Forbidden | 403 | Illegal access to a resource occurs. |
NotDirectoryException | BadRequest | 400 | The specified path is not a directory. |
IllegalArgumentException | BadRequest | 400 | An invalid argument is provided. |
java.io.FileNotFoundException | NotFound | 404 | The specified file or resource is not found. |
java.net.ConnectException | ServiceUnavailable | 503 | Unable to connect to a service. |
java.net.SocketTimeoutException | GatewayTimeout | 504 | Connection or read operation timed out. |
Others (unrecognized throwable) | InternalServerError | 500 | An 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@48efc330
failedHandler.mapErrorCause(Response.fromCause)
// res10: Handler[Any, Response, Any, Nothing] = zio.http.Handler$$anon$4@3597771
Failure Responses with Details​
By default, the Response.fromThrowable
and Response.fromCause
methods create a response with a status code only. If we want to include additional details in the response, we have to hand over a ErrorResponseConfig
.
/**
* Configuration for the response generation
*
* @param withErrorBody
* if true, includes the error message in the response body
* @param withStackTrace
* if true, includes the stack trace in the response body
* @param maxStackTraceDepth
* maximum number of stack trace lines to include in the response body. Set to
* 0 to include all lines.
* @param errorFormat
* the preferred format for the error response.
* If the context in which the response is created has access to an Accept header,
* the header will be used preferably to determine the format.
*/
final case class ErrorResponseConfig(
withErrorBody: Boolean = false,
withStackTrace: Boolean = false,
maxStackTraceDepth: Int = 10,
errorFormat: ErrorResponseConfig.ErrorFormat = ErrorResponseConfig.ErrorFormat.Html,
)
This config can not only be used directly, but can also configure how ZIO-HTTP internally converts a Cause
or Throwable
to a Response
.
You can configure error responses globally by providing a custom ErrorResponseConfig
via layer for example in the bootstrap of your application.
Or you can apply the config locally to some routes via middleware.
import zio.http._
object MyHttpApp extends ZIOAppDefault {
// Provide a custom ErrorResponseConfig via layer
// Equivalent to: val bootstrap = ErrorResponseConfig.configLayer(ErrorResponseConfig.debugConfig)
override val bootstrap: ZLayer[ZIOAppArgs, Any, Any] = ErrorResponseConfig.debugLayer
// Apply the ErrorResponseConfig.debug middleware to routes
// Equivalent to: val myRoutes = Handler.ok.toRoutes @@ ErrorResponseConfig.withConfig(ErrorResponseConfig.debugConfig)
val myRoutes = Handler.ok.toRoutes @@ ErrorResponseConfig.debug
override def run = ???
}
The debug config will include the error message and full stack trace in the response body.
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.
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'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.