HTTP Client
The Client
trait in http4s can submit a Request
to a server and return a Response
.
trait Client[F[_]] {
def run(req: Request[F]): Resource[F, Response[F]]
//...
}
While Client
is abstract in its effect type F
, we will use concrete IO
throughout this guide.
Let's briefly chat about the Resource
wrapping the return type.
Every request/response pair is transmitted over a connection, which is a finite resource.
When you are done reading the Response
, you return from the Resource
.
This releases the connection so that it may be re-used by another request/response pair, or shutdown.
Here's a quick example app to print the response of a GET request.
import cats.effect.{IO, IOApp}
import org.http4s.ember.client.EmberClientBuilder
import org.http4s.client.Client
import org.typelevel.log4cats.LoggerFactory
import org.typelevel.log4cats.slf4j.Slf4jFactory
object Hello extends IOApp.Simple {
private implicit val loggerFactory: LoggerFactory[IO] =
Slf4jFactory.create[IO]
def printHello(client: Client[IO]): IO[Unit] =
client
.expect[String]("http://localhost:8080/hello/Ember")
.flatMap(IO.println)
val run: IO[Unit] = EmberClientBuilder
.default[IO]
.build
.use(client => printHello(client))
}
Setup
In order to play with a Client
we'll first create an http4s Server
.
Ensure you have the following dependencies in your build.sbt:
scalaVersion := "2.13.15" // Also supports 3.x
val http4sVersion = "1.0.0-M43"
libraryDependencies ++= Seq(
"org.http4s" %% "http4s-ember-client" % http4sVersion,
"org.http4s" %% "http4s-ember-server" % http4sVersion,
"org.http4s" %% "http4s-dsl" % http4sVersion,
"org.typelevel" %% "log4cats-slf4j" % log4catsVersion,
)
Now we can finish setting up our server:
import com.comcast.ip4s._
import org.http4s._
import org.http4s.dsl.io._
import org.http4s.implicits._
import org.http4s.ember.server.EmberServerBuilder
import org.http4s.server.middleware.Logger
import org.typelevel.log4cats.LoggerFactory
import org.typelevel.log4cats.slf4j.Slf4jFactory
implicit val loggerFactory: LoggerFactory[IO] = Slf4jFactory.create[IO]
val app = HttpRoutes.of[IO] {
case GET -> Root / "hello" / name =>
Ok(s"Hello, $name.")
}.orNotFound
val finalHttpApp = Logger.httpApp(true, true)(app)
val server = EmberServerBuilder
.default[IO]
.withHost(ipv4"0.0.0.0")
.withPort(port"8080")
.withHttpApp(finalHttpApp)
.build
Because this documentation is running in mdoc we need an implicit IORuntime
to let us run our IO
values explicitly with .unsafeRunSync()
.
In real code you should construct your whole program in IO
and assign it to run
in IOApp
as in the example above.
import cats.effect.unsafe.IORuntime
implicit val runtime: IORuntime = IORuntime.global
If you are following along in a REPL you will need to start the server in the background. Additionally you will want a way to shutdown the server when you're done.
You can do this by starting the server like so:
val shutdown = server.allocated.unsafeRunSync()._2
Later you can call shutdown.unsafeRunSync()
to run the server's finalizers and release resources.
Making Requests
Creating the client
A good default choice is the EmberClientBuilder
.
The EmberClientBuilder
sets up a connection pool, enabling the reuse of connections for multiple requests, supports HTTP/1.x and HTTP/2, and is available for ScalaJS.
import org.http4s.ember.client.EmberClientBuilder
EmberClientBuilder
.default[IO]
.build
.use { client =>
// use `client` here, returning an `IO`.
client.expect[String]("http://localhost:8080/hello/Ember")
}
In the above example .build
returns a Resource[IO, Client]
.
We use the Client
by passing use
a function Client => IO[B]
.
The result is a value that, when run, will acquire a Client
, use it, and release it (even under cancellation or errors).
Note that we generally only call .build.use
once per application and pass around the Client
.
See the Quick Start g8 template for an example.
For the remainder of this tutorial, we'll use an alternate client backend
built on the standard java.net
library client. Unlike the ember
client, it does not need to be shut down. Like the ember-client, and
any other http4s backend, it presents the exact same Client
interface!
It uses blocking I/O and is not suited for production, but it is highly useful in a REPL or mdoc documentation:
import org.http4s.client.JavaNetClientBuilder
// for REPL or mdoc use only!
val httpClient: Client[IO] = JavaNetClientBuilder[IO].create
Describing a Request
To execute a GET request, we can call expect
with the type we expect
and the URI we want:
val helloEmber: IO[String] =
httpClient.expect[String]("http://localhost:8080/hello/Ember")
We don't have any output from the server yet as we have not executed the request.
We have an IO[String]
value, which is a description of a program that, when run,
will send a GET request to the server, and expect a plain text String
response.
Let's build another program that makes requests in parallel to greet a collection of people:
import cats.effect.IO
import cats.syntax.all._
import org.http4s.Uri
def hello(name: String): IO[String] = {
val target = uri"http://localhost:8080/hello/" / name
httpClient.expect[String](target)
}
val inputs = List("Ember", "http4s", "Scala")
val getGreetings: IO[List[String]] =
inputs.parTraverse(hello)
We use parTraverse
to apply hello
to each name and collect the results into
one IO[List[String]]
.
The par
prefix (as in "parallel") on parTraverse
indicates that this
will happen concurrently not sequentially.
Running a Request
We have built two programs: helloEmber
will make a single request to get a greeting for Ember,
and getGreetings
will make multiple concurrent requests getting multiple greetings.
In a production application we would likely compose these programs with other programs
up until we finally pass them to run
in IOApp
as seen in our intro example.
Here in mdoc, or in a REPL, we manually run the IO
with unsafeRunSync()
.
Remember, you should not do this in your applications.
helloEmber.unsafeRunSync()
// res1: String = "Hello, Ember."
getGreetings.unsafeRunSync()
// res2: List[String] = List(
// "Hello, Ember.",
// "Hello, http4s.",
// "Hello, Scala."
// )
Constructing a URI
Typically, to construct a Request
, you use a Uri
to represent the endpoint you
want to access.
There are a number of ways to construct a Uri
.
If you have a literal string, you can use the uri
string interpolator:
uri"https://my-awesome-service.com/foo/bar?wow=yeah"
// res3: Uri = Uri(
// scheme = Some(value = Scheme(value = https)),
// authority = Some(
// value = Authority(
// userInfo = None,
// host = RegName(host = my-awesome-service.com),
// port = None
// )
// ),
// path = /foo/bar,
// query = wow=yeah,
// fragment = None
// )
This only works with literal strings because it uses a macro to validate the URI format at compile-time.
Otherwise, you'll need to use Uri.fromString(...)
and handle the case where
validation fails:
val validUri = "https://my-awesome-service.com/foo/bar?wow=yeah"
val invalidUri = "yeah whatever"
val uri: ParseResult[Uri] = Uri.fromString(validUri)
// uri: ParseResult[Uri] = Right(
// value = Uri(
// scheme = Some(value = Scheme(value = https)),
// authority = Some(
// value = Authority(
// userInfo = None,
// host = RegName(host = my-awesome-service.com),
// port = None
// )
// ),
// path = /foo/bar,
// query = wow=yeah,
// fragment = None
// )
// )
val parseFailure: ParseResult[Uri] = Uri.fromString(invalidUri)
// parseFailure: ParseResult[Uri] = Left(
// value = ParseFailure(
// sanitized = "Invalid URI",
// details = """yeah whatever
// ^
// expectation:
// * must end the string"""
// )
// )
You can also build up a URI incrementally, e.g.:
val baseUri: Uri = uri"http://foo.com"
val withPath: Uri = baseUri.withPath(path"/bar/baz")
val withQuery: Uri = withPath.withQueryParam("hello", "world")
Middleware
Like the server middleware, the client middleware is a wrapper around a
Client
that provides a means of accessing or manipulating Request
s
and Response
s being sent.
Consider functions from Int
to String
. We could create a wrapper over functions of this type,
which would take an Int => String
and return an Int => String
.
Such a wrapper could make the result inspect its input, do something to it, and call the original function with that input (or even another one). Then it could look at the response and also make some actions based on it.
An example wrapper could look something like this:
def mid(f: Int => String): Int => String = in => {
// `in` is the input originally passed to the function.
// We can pass it to `f` directly.
// Or use it to construct a new value.
val resultOfF = f(in + 1)
// `resultOfF` is the result of the function applied to the new input.
// Similarly, we can return it directly, or build a new value.
s"$in was incremented to yield $resultOfF"
}
If we wrap a simple function, say, one returning the String representation of a number:
val f1: Int => String = _.toString
// f1: Int => String = <function1>
// Here, we're applying our wrapper to `f1`. Notice that this is still a function.
val f2: Int => String = mid(f1)
// f2: Int => String = <function1>
f1(10)
// res4: String = "10"
f2(10)
// res5: String = "10 was incremented to yield 11"
We see how f2
wraps f1
by passing an incremented argument to the original function.
This wrapper could be considered a middleware over functions from Int
to String
.
Recall our simplified definition of Client[F]
- it boils down to a single abstract method:
trait Client[F[_]] {
def run(request: Request[F]): Resource[F, Response[F]]
}
Knowing this, we could say a Client[F]
is equivalent to a function from Request[F]
to Resource[F, Response[F]]
. In fact, given a client, we could call client.run _
to get that function.
A client middleware follows the same idea as our original middleware did: it takes a Client
(which is a function) and returns another Client
(which is also a function).
It can see the input Request[F]
that we pass to the client when we call it, it can modify that request, pass it to the underlying client (or any other client, really!), and do all sorts of other things, including effects - all it has to do is return a Resource[F, Response[F]]
.
The real definition of Client
is a little more complicated because there's several more abstract methods.
If you want to implement a client using just a function (for example, to make a middleware), consider using Client.apply
.
A simple middleware, which would add a constant header to every request and response, could look like this:
import cats.effect.MonadCancelThrow
import org.typelevel.ci._
def addTestHeader[F[_]: MonadCancelThrow](underlying: Client[F]): Client[F] = Client[F] { req =>
underlying
.run(
req.withHeaders(Header.Raw(ci"X-Test-Request", "test"))
)
.map(
_.withHeaders(Header.Raw(ci"X-Test-Response", "test"))
)
}
As the caller of the client you would get from this, you would see the extra header in the response. Similarly, every service called by the client would see an extra header in the requests.
Included Middleware
Http4s includes some middleware Out of the Box in the org.http4s.client.middleware
package. These include:
- Following of redirect responses (Follow Redirect)
- Retrying of requests (Retry)
- Metrics gathering (Metrics)
- Logging of requests (Request Logger)
- Logging of responses (Response Logger)
- Logging of requests and responses (Logger)
Metrics Middleware
Apart from the middleware mentioned in the previous section. There is, as well, Out of the Box middleware for Dropwizard and Prometheus metrics.
Examples
Send a GET request
You can send a GET by calling the expect
method on the client, passing a Uri
:
httpClient.expect[String](uri"https://google.com/")
If you need to do something more complicated like setting request headers, you
can build up a request object and pass that to expect
:
import cats.effect.IO
import org.http4s.Request
import org.http4s.Headers
import org.http4s.headers._
import org.http4s.MediaType
val request = Request[IO](
method = Method.GET,
uri = uri"https://my-lovely-api.com/",
headers = Headers(
Authorization(Credentials.Token(AuthScheme.Bearer, "open sesame")),
Accept(MediaType.application.json),
)
)
httpClient.expect[String](request)
Send a POST request
You can send a POST request and decode the JSON response into a case class
by deriving an EntityDecoder
for that case class:
import cats.effect.IO
import org.http4s.circe._
import io.circe.generic.auto._
case class AuthResponse(access_token: String)
implicit val authResponseEntityDecoder: EntityDecoder[IO, AuthResponse] = jsonOf
val postRequest = Request[IO](
method = Method.POST,
uri = uri"https://my-lovely-api.com/oauth2/token"
).withEntity(
UrlForm(
"grant_type" -> "client_credentials",
"client_id" -> "my-awesome-client",
"client_secret" -> "s3cr3t"
)
)
httpClient.expect[AuthResponse](postRequest)
Calls to a JSON API
Take a look at json.
Body decoding / encoding
The reusable way to decode or encode a request is to write a custom EntityDecoder
or EntityEncoder
. For that topic, take a look at entity.
If you prefer a more fine-grained approach, some of the methods on Client
take a
Response[F] => F[A]
argument, such as get
, which lets you add a function which
includes the decoding functionality, but ignores the media type.
val endpoint = uri"http://localhost:8080/hello/Ember"
httpClient.get[Either[String, String]](endpoint) {
case Status.Successful(r) => r.attemptAs[String].leftMap(_.message).value
case r => r.as[String]
.map(b => Left(s"Request failed with status ${r.status.code} and body $b"))
}
Your function has to consume the body before the returned F
exits.
Response.entity
yields an Entity[F]
that encodes body of the response.
A response body may be empty, completely loaded into memory, or streamed.
For more details, see the entity page. In the case of a streamed response,
it's this Stream
that needs to be consumed within your effect F
.
Do not do this:
import org.http4s.EntityBody
// response.body is not consumed within `F`
httpClient.get[EntityBody[IO]]("some-url")(response => IO(response.body))