Http
Http
is a functional domain that models HTTP applications. It’s polymorphic on input and output type.
A Http[-R, +E, -A, +B]
models a function from A
to ZIO[R, Option[E], B]
. When a value of type A
is evaluated against an Http[R,E,A,B]
, it can either succeed with a B
, fail with a Some[E]
or if A
is not defined in the application, fail with None
.
Http
domain provides several operators and constructors to model the application as per your use case.
Creating an HTTP Application
HTTP application that always succeeds
To create an HTTP application that always returns the same response and never fails, you can use the succeed
constructor.
val app: Http[Any, Nothing, Any, Int] = Http.succeed(1)
HTTP application that always fails
To create an HTTP application that always fails with the given error, you can use the fail
constructor.
val app: Http[Any, Error, Any, Nothing] = Http.fail(new Error("Error_Message"))
HTTP applications can also be created from total and partial functions. These are some constructors to create HTTP applications from total as well as partial functions.
HTTP application from a partial function
Http.Collect
can create an Http[Any, Nothing, A, B]
from a PartialFunction[A, B]
. In case the input is not defined for the partial function, the application will return a None
type error.
val app: Http[Any, Nothing, String, String] = Http.collect[String] {
case "case 1" => "response 1"
case "case 2" => "response 2"
}
Http.CollectZIO
can be used to create a Http[R, E, A, B]
from a partial function that returns a ZIO effect, i.e PartialFunction[A, ZIO[R, E, B]
. This constructor is used when the output is effectful.
val app: Http[Any, Nothing, String, String] = Http.collectZIO[String] {
case "case 1" => ZIO.succeed("response 1")
}
HTTP application from a total function
Http.fromFunction
can create an Http[Any, Nothing, A, B]
from a function f: A=>B
.
val app: Http[Any, Nothing, Int, Int] = Http.fromFunction[Int](i => i + 1)
Http.fromFunctionZIO
can create a Http[R, E, A, B]
from a function that returns a ZIO effect, i.e f: A => ZIO[R, E, B]
.
val app: Http[Any, Nothing, Int, Int] = Http.fromFunction[Int](i => ZIO.succeed(i + 1))
Transforming Http Applications
Http operators are used to transform one or more HTTP applications to create a new HTTP application. Http domain provides plenty of such powerful operators.
Transforming the output
To transform the output of the HTTP application, you can use map
operator . It takes a function f: B=>C
to convert a Http[R,E,A,B]
to Http[R,E,A,C]
.
val a: Http[Any, Nothing, Any, String] = Http.succeed("text")
val app: Http[Any, Nothing, Any, Int] = a.map(s => s.length())
To transform the output of the HTTP application effectfully, you can use mapZIO
operator. It takes a function B => ZIO[R1, E1, C]
to convert a Http[R,E,A,B]
to Http[R,E,A,C]
.
val a: Http[Any, Nothing, Any, String] = Http.succeed("text")
val app: Http[Any, Nothing, Any, Int] = a.mapZIO(s => ZIO.succeed(s.length()))
Transforming the input
To transform the input of the HTTP application, you can use contramap
operator. Before passing the input on to the HTTP application, contramap
applies a function xa: X => A
on it.
val a: Http[Any, Nothing, String, String] = Http.fromFunction[String](s => s + ' ' + s)
val app: Http[Any, Nothing, Int, String] = a.contramap[Int](_.toString)
To transform the input of the HTTP application effectfully, you can use contramapZIO
operator. Before passing the input on to the HTTP application, contramapZIO
applies a function xa: X => ZIO[R1, E1, A]
on it.
val a: Http[Any, Nothing, String, String] = Http.fromFunction[String](s => s + ' ' + s)
val app: Http[Any, Any, Int, String] = a.contramapZIO[Any, Any,Int](a=>ZIO.succeed(a.toString))
Chaining HTTP applications
To chain two HTTP applications, you can use flatMap
operator.It creates a new Http[R1, E1, A1, C1]
from the output of a Http[R,E,A,B]
, using a function f: B => Http[R1, E1, A1, C1]
. >>=
is an alias for flatMap.
val a: Http[Any, Nothing, Any, String] = Http.succeed("text1")
val app: Http[Any, Nothing, Any, String] = a >>= (s => Http.succeed(s + " text2"))
Folding an HTTP application
foldHttp
lets you handle the success and failure values of an HTTP application. It takes in two functions, one for failure and one for success, and one more HTTP application.
- If the application fails with
Some[E]
the first function will be executed withE
, - If the application succeeds with
B
, the second function will be executed withB
and - If the application fails with
None
the given HTTP application will be executed with the original input.
val a: Http[Any, String, String, String] = Http.collectHttp[String]{
case "case" => Http.fail("1")
case _ => Http.succeed("2")
}
val b: Http[Any, Nothing, Any, String] = Http.succeed("3")
val app: Http[Any, Nothing, String, String] = a.foldHttp(e => Http.succeed(e), s => Http.succeed(s), b)
Error Handling
These are several ways in which error handling can be done in Http
domain,
Catch all errors
To catch all errors in case of failure of an HTTP application, you can use catchAll
operator. It pipes the error to a function f: E => Http[R1, E1, A1, B1]
.
val a: Http[Any, Throwable, Any, Nothing] = Http.fail(new Throwable("Error_Message"))
val app: Http[Any, Nothing, Any, Option[Throwable]] = a.catchAll(e => Http.succeed(Option(e)))
Mapping the error
To transform the failure of an HTTP application, you can use mapError
operator. It pipes the error to a function ee: E => E1
.
val a: Http[Any, Throwable, Any, Nothing] = Http.fail(new Throwable("Error_Message"))
val app: Http[Any, Option[Throwable], Any, Nothing] = a.mapError(e => Option(e))
Composition of HTTP applications
HTTP applications can be composed using several special operators.
Using ++
++
is an alias for defaultWith
. While using ++
, if the first HTTP application returns None
the second HTTP application will be evaluated, ignoring the result from the first. If the first HTTP application is failing with a Some[E]
the second HTTP application won't be evaluated.
val a: Http[Any, Nothing, String, String] = Http.collect[String] {
case "case 1" => "response 1"
case "case 2" => "response 2"
}
val b: Http[Any, Nothing, String, String] = Http.collect[String] {
case "case 3" => "response 3"
case "case 4" => "response 4"
}
val app: Http[Any, Nothing, String, String] = a ++ b
Using <>
<>
is an alias for orElse
. While using <>
, if the first HTTP application fails with Some[E]
, the second HTTP application will be evaluated, ignoring the result from the first. If the first HTTP application returns None
, the second HTTP application won't be evaluated.
val app: Http[Any, Nothing, Any, Int] = Http.fail(1) <> Http.succeed(2)
Using >>>
>>>
is an alias for andThen
. It runs the first HTTP application and pipes the output into the other.
val a: Http[Any, Nothing, Int, Int] = Http.fromFunction[Int](a => a + 1)
val b: Http[Any, Nothing, Int, Int] = Http.fromFunction[Int](b => b * 2)
val app: Http[Any, Nothing, Int, Int] = a >>> (b)
Using <<<
<<<
is the alias for compose
. Compose is similar to andThen. It runs the second HTTP application and pipes the output to the first HTTP
application.
val a: Http[Any, Nothing, Int, Int] = Http.fromFunction[Int](a => a + 1)
val b: Http[Any, Nothing, Int, Int] = Http.fromFunction[Int](b => b * 2)
val app: Http[Any, Nothing, Int, Int] = a <<< (b)
Providing environments
There are many operators to provide the HTTP application with its required environment.
provideCustomLayer
Provides the HTTP application with the part of the environment that is not part of the ZEnv, leaving an effect that only depends on the ZEnv.
val a: Http[Clock, DateTimeException, String, OffsetDateTime] = Http.collectZIO[String] {
case "case 1" => clock.currentDateTime
}
val app: Http[zio.ZEnv, DateTimeException, String, OffsetDateTime] = a.provideCustomLayer(Clock.live)
Attaching Middleware
Middlewares are essentially transformations that one can apply to any Http
to produce a new one. To attach middleware to the HTTP application, you can use middleware
operator. @@
is an alias for middleware
.
val app: Http[Any, Int, Any, Nothing] = Http.succeed(1) @@ Middleware.fail(2)
Unit testing
Since an HTTP application Http[R, E, A, B]
is a function from A
to ZIO[R, Option[E], B]
, we can write unit tests just like we do for normal functions.
The below snippet tests an app that takes Int
as input and responds by adding 1 to the input.
package zio.http.middleware
import zio.http._
import zio.test.Assertion.equalTo
import zio.test._
object Spec extends DefaultRunnableSpec {
def spec = suite("http")(
test("1 + 1 = 2") {
val app: Http[Any, Nothing, Int, Int] = Http.fromFunction[Int](_ + 1)
assertZIO(app(1))(equalTo(2))
}
)
}
What is HttpApp?
HttpApp[-R, +E]
is a type alias for Http[R, E, Request, Response]
, i.e HttpApp[-R, +E]
is a function
from Request
to ZIO[R, Option[E], Response]
. ZIO HTTP server runs HttpApp[R, E]
only.
Special Constructors for HttpApp
These are some of the special constructors for HttpApp:
Http.ok
Creates an HttpApp
that always responds with a 200 status code.
val app: HttpApp[Any, Nothing] = Http.ok
Http.text
Creates an HttpApp
that always responds with the same plain text.
val app: HttpApp[Any, Nothing] = Http.text("Text Response")
Http.status
Creates an HttpApp
that always responds with the same status code and empty data.
val app: HttpApp[Any, Nothing] = Http.status(Status.OK)
Http.error
Creates an HttpApp
that always fails with the given HttpError
.
val app: HttpApp[Any, Nothing] = Http.error(HttpError.Forbidden())
Http.response
Creates an HttpApp
that always responds with the same Response
.
val app: HttpApp[Any, Nothing] = Http.response(Response.ok)
Special operators on HttpApp
These are some special operators for HttpApps
.
setMethod
Overwrites the method in the incoming request to the HttpApp
val a: HttpApp[Any, Nothing] = Http.collect[Request] {
case Method.GET -> !! / "text" => Response.text("Hello World!")
}
val app = a setMethod (Method.POST)
patch
Patches the response produced by the HTTP application using a Patch
.
val a: HttpApp[Any, Nothing] = Http.collect[Request] {
case Method.GET -> !! / "text" => Response.text("Hello World!")
}
val app = a.patch(Patch.setStatus(Status.ACCEPTED))
getBodyAsString
getBodyAsString
extract the body of the response as a string and make it the output type.
val a: HttpApp[Any, Nothing] = Http.collect[Request] {
case Method.GET -> !! / "text" => Response.text("Hello World!")
}
val app: Http[Any, Throwable, Request, String] = a.bodyAsString
Converting an Http
to HttpApp
If you want to run an Http[R, E, A, B]
app on the ZIO HTTP server you need to convert it to HttpApp[R, E]
using operators
like map
, contramap
, codec
etc.
Using map and contramap
Below snippet shows an app of type Http
which takes a string and responds with a string.
val http: Http[Any, Nothing, String, String] = Http.collect[String] {
case "GET" => "Ok"
}
Now, to convert it into an HttpApp
- use
contramap
to transform the input ieString
toRequest
- use
map
to transform the output ieString
toResponse
val app: HttpApp[Any, Nothing] = http.contramap[Request](r => r.method.toString()).map[Response](s => Response.text(s))
Using middleware
We can also convert an Http
to HttpApp
using codec middlewares that take in 2 functions decoder: AOut => Either[E, AIn]
and
encoder: BIn => Either[E, BOut]
. Please find more operators in middlewares.
val a: Http[Any, Nothing, String, String] = Http.collect[String] {
case "GET" => "Ok"
}
val app: Http[Any, Nothing, Request, Response] = a @@ Middleware.codec[Request,String](r => Right(r.method.toString()),s => Right(Response.text(s)))
Running an HttpApp
ZIO HTTP server needs an HttpApp[R,E]
for running.
We can use Server.app()
method to bootstrap the server with an HttpApp[R,E]
import zio.http._
import zio.http.Server
import zio._
object HelloWorld extends App {
val app: HttpApp[Any, Nothing] = Http.ok
override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = Server.start(8090, app).exitCode
}