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@3182d2cf
failedHandler.mapErrorCause(Response.fromCause)
// res10: Handler[Any, Response, Any, Nothing] = zio.http.Handler$$anon$4@ba6addb
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.