HTTP Client

How do we know the server is running? Let's create a client with http4s to try our service.

A recap of the dependencies for this example, in case you skipped the service example. Ensure you have the following dependencies in your build.sbt:

scalaVersion := "2.13.8" // Also supports 2.11.x and 2.12.x

val http4sVersion = "1.0.0-M32"

// Only necessary for SNAPSHOT releases
resolvers += Resolver.sonatypeRepo("snapshots")

libraryDependencies ++= Seq(
  "org.http4s" %% "http4s-dsl" % http4sVersion,
  "org.http4s" %% "http4s-ember-server" % http4sVersion,
  "org.http4s" %% "http4s-ember-client" % http4sVersion
)

Then we create the service again so mdoc picks it up: >

import cats.effect._
import com.comcast.ip4s._
import org.http4s._
import org.http4s.dsl.io._
import org.http4s.implicits._
import org.http4s.ember.server._
import org.http4s.server.middleware.Logger
import scala.concurrent.duration._

The following is provided by an IOApp, but necessary if following along in a REPL:

import cats.effect.unsafe.IORuntime
implicit val runtime: IORuntime = cats.effect.unsafe.IORuntime.global

Finish setting up our server:

val app = HttpRoutes.of[IO] {
  case GET -> Root / "hello" / name =>
    Ok(s"Hello, $name.")
}.orNotFound
// app: cats.data.Kleisli[IO[A], Request[IO], Response[IO[A]]] = Kleisli(
//   run = org.http4s.syntax.KleisliResponseOps$$Lambda$22167/2118619545@230cd9a6
// )

val finalHttpApp = Logger.httpApp(true, true)(app)
// finalHttpApp: HttpApp[IO[A]] = Kleisli(
//   run = org.http4s.server.middleware.ResponseLogger$$$Lambda$22177/2135324045@2694b1bb
// )

val server = EmberServerBuilder
  .default[IO]
  .withHost(ipv4"0.0.0.0")
  .withPort(port"8080")
  .withHttpApp(finalHttpApp)
  .build
// server: Resource[IO, org.http4s.server.Server] = Bind(
//   source = Pure(a = fs2.io.net.NetworkCompanionPlatform$$anon$1@2660f166),
//   fs = org.http4s.ember.server.EmberServerBuilder$$Lambda$22286/1234921710@3e008bce
// )

We'll start the server in the background.

val shutdown = server.allocated.unsafeRunSync()._2
// shutdown: IO[Unit] = Uncancelable(
//   body = cats.effect.IO$$Lambda$22644/1319364585@2e770df7,
//   event = cats.effect.tracing.TracingEvent$StackTrace
// )

Creating the client

A good default choice is the EmberClientBuilder. The EmberClientBuilder maintains a connection pool and speaks HTTP 1.x.

import org.http4s.ember.client._
import org.http4s.client._
EmberClientBuilder.default[IO].build.use { client =>
  // use `client` here and return an `IO`.
  // the client will be acquired and shut down
  // automatically each time the `IO` is run.
  IO.unit
}

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 IO and is less suited for production, but it is highly useful in a REPL:

import java.util.concurrent._

val blockingPool = Executors.newFixedThreadPool(5)
val httpClient: Client[IO] = JavaNetClientBuilder[IO].create

Describing a call

To execute a GET request, we can call expect with the type we expect and the URI we want:

val helloJames = httpClient.expect[String]("http://localhost:8080/hello/James")
// helloJames: IO[String] = FlatMap(
//   ioe = Delay(
//     thunk = org.http4s.client.JavaNetClientBuilder$$Lambda$22685/338499742@4e13b02e,
//     event = cats.effect.tracing.TracingEvent$StackTrace
//   ),
//   f = cats.effect.kernel.Resource$$Lambda$22665/239848797@633ab5fa,
//   event = cats.effect.tracing.TracingEvent$StackTrace
// )

Note that we don't have any output yet. We have a IO[String], to represent the asynchronous nature of a client request.

Furthermore, we haven't even executed the request yet. A significant difference between a IO and a scala.concurrent.Future is that a Future starts running immediately on its implicit execution context, whereas a IO runs when it's told. Executing a request is an example of a side effect. In functional programming, we prefer to build a description of the program we're going to run, and defer its side effects to the end.

Let's describe how we're going to greet a collection of people in parallel:

import cats._, cats.effect._, cats.implicits._
import org.http4s.Uri
def hello(name: String): IO[String] = {
  val target = uri"http://localhost:8080/hello/" / name
  httpClient.expect[String](target)
}

val people = Vector("Michael", "Jessica", "Ashley", "Christopher")
// people: Vector[String] = Vector(
//   "Michael",
//   "Jessica",
//   "Ashley",
//   "Christopher"
// )

val greetingList = people.parTraverse(hello)
// greetingList: IO[Vector[String]] = Map(
//   ioe = Map(
//     ioe = Uncancelable(
//       body = cats.effect.kernel.instances.GenSpawnInstances$$anon$4$$Lambda$22690/1407339097@41ec15cc,
//       event = cats.effect.tracing.TracingEvent$StackTrace
//     ),
//     f = cats.data.Chain$$$Lambda$21980/111004944@48f42afc,
//     event = cats.effect.tracing.TracingEvent$StackTrace
//   ),
//   f = cats.instances.VectorInstances$$anon$1$$Lambda$22691/1148774657@35853f20,
//   event = cats.effect.tracing.TracingEvent$StackTrace
// )

Observe how simply we could combine a single F[String] returned by hello into a scatter-gather to return a F[List[String]].

Making the call

It is best to run your F "at the end of the world." The "end of the world" varies by context:

val greetingsStringEffect = greetingList.map(_.mkString("\n"))
// greetingsStringEffect: IO[String] = Map(
//   ioe = Map(
//     ioe = Map(
//       ioe = Uncancelable(
//         body = cats.effect.kernel.instances.GenSpawnInstances$$anon$4$$Lambda$22690/1407339097@41ec15cc,
//         event = cats.effect.tracing.TracingEvent$StackTrace
//       ),
//       f = cats.data.Chain$$$Lambda$21980/111004944@48f42afc,
//       event = cats.effect.tracing.TracingEvent$StackTrace
//     ),
//     f = cats.instances.VectorInstances$$anon$1$$Lambda$22691/1148774657@35853f20,
//     event = cats.effect.tracing.TracingEvent$StackTrace
//   ),
//   f = <function1>,
//   event = cats.effect.tracing.TracingEvent$StackTrace
// )
greetingsStringEffect.unsafeRunSync()
// res1: String = """Hello, Michael.
// Hello, Jessica.
// Hello, Ashley.
// Hello, Christopher."""

Constructing a URI

Before you can make a call, you'll need 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 uri"...":

uri"https://my-awesome-service.com/foo/bar?wow=yeah"
// res2: Uri = Uri(
//   scheme = Some(value = Scheme(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"
// validUri: String = "https://my-awesome-service.com/foo/bar?wow=yeah"
val invalidUri = "yeah whatever"
// invalidUri: String = "yeah whatever"

val uri: Either[ParseFailure, Uri] = Uri.fromString(validUri)
// uri: Either[ParseFailure, Uri] = Right(
//   value = Uri(
//     scheme = Some(value = Scheme(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: Either[ParseFailure, Uri] = Uri.fromString(invalidUri)
// parseFailure: Either[ParseFailure, Uri] = Left(
//   value = ParseFailure(
//     sanitized = "Invalid URI",
//     details = "Error(4,NonEmptyList(EndOfString(4,13)))"
//   )
// )

You can also build up a URI incrementally, e.g.:

val baseUri: Uri = uri"http://foo.com"
// baseUri: Uri = Uri(
//   scheme = Some(value = Scheme(http)),
//   authority = Some(
//     value = Authority(
//       userInfo = None,
//       host = RegName(host = foo.com),
//       port = None
//     )
//   ),
//   path = ,
//   query = ,
//   fragment = None
// )
val withPath: Uri = baseUri.withPath(path"/bar/baz")
// withPath: Uri = Uri(
//   scheme = Some(value = Scheme(http)),
//   authority = Some(
//     value = Authority(
//       userInfo = None,
//       host = RegName(host = foo.com),
//       port = None
//     )
//   ),
//   path = /bar/baz,
//   query = ,
//   fragment = None
// )
val withQuery: Uri = withPath.withQueryParam("hello", "world")
// withQuery: Uri = Uri(
//   scheme = Some(value = Scheme(http)),
//   authority = Some(
//     value = Authority(
//       userInfo = None,
//       host = RegName(host = foo.com),
//       port = None
//     )
//   ),
//   path = /bar/baz,
//   query = hello=world,
//   fragment = None
// )

Middleware

Like the server middleware, the client middleware is a wrapper around a Client that provides a means of accessing or manipulating Requests and Responses being sent.

Consider functions from Int to String. We could create a wrapper over functions of this type, which would take anInt => Stringand return anInt => 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 => {
  // here, `in` is the input originally passed to the function
  // we can decide to pass it to `f`, or modify it first. We'll change it for the example.
  val resultOfF = f(in + 1)

  // Now, `resultOfF` is the result of the function applied with the modified result.
  // We can return it verbatim or _also_ modify it first! We could even ignore it.
  // Here, we'll use both results - the one we got from the original call (f(in)) and the customized one (f(in + 1)).
  s"${f(in)} is the original result, but $resultOfF's input was modified!"
}

If we were to 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)
// res3: String = "10"
f2(10)
// res4: String = "10 is the original result, but 11's input was modified!"

We would see how it's changing the result of the f1 function by giving it another input.

This wrapper could be considered a middleware over functions from Int to String. Now consider a 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 org.typelevel.ci.CIString

def addTestHeader[F[_]: MonadCancelThrow](underlying: Client[F]): Client[F] = Client[F] { req =>
  underlying
    .run(
      req.withHeaders(Header.Raw(CIString("X-Test-Request"), "test"))
    )
    .map(
      _.withHeaders(Header.Raw(CIString("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:

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

Dropwizard Metrics Middleware

To make use of this metrics middleware the following dependencies are needed:

libraryDependencies ++= Seq(
  "org.http4s" %% "http4s-client" % http4sVersion,
  "org.http4s" %% "http4s-dropwizard-metrics" % http4sVersion
)

We can create a middleware that registers metrics prefixed with a provided prefix like this.

import org.http4s.client.middleware.Metrics
import org.http4s.metrics.dropwizard.Dropwizard
import com.codahale.metrics.SharedMetricRegistries
val registry = SharedMetricRegistries.getOrCreate("default")
// registry: com.codahale.metrics.MetricRegistry = com.codahale.metrics.MetricRegistry@34f97665
val requestMethodClassifier = (r: Request[IO]) => Some(r.method.toString.toLowerCase)
// requestMethodClassifier: Request[IO] => Some[String] = <function1>

val meteredClient =
  Metrics[IO](Dropwizard(registry, "prefix"), requestMethodClassifier)(httpClient)
// meteredClient: Client[IO] = org.http4s.client.Client$$anon$3@54cb57b6

A classifier is just a function Request[F] => Option[String] that allows to add a subprefix to every metric based on the Request

Prometheus Metrics Middleware

To make use of this metrics middleware the following dependencies are needed:

libraryDependencies ++= Seq(
  "org.http4s" %% "http4s-client" % http4sVersion,
  "org.http4s" %% "http4s-prometheus-metrics" % http4sVersion
)

We can create a middleware that registers metrics prefixed with a provided prefix like this.

import cats.effect.{Resource, IO}
import org.http4s.client.middleware.Metrics
import org.http4s.metrics.prometheus.Prometheus
val classifier = (r: Request[IO]) => Some(r.method.toString.toLowerCase)
// classifier: Request[IO] => Some[String] = <function1>

val prefixedClient: Resource[IO, Client[IO]] =
  for {
    registry <- Prometheus.collectorRegistry[IO]
    metrics <- Prometheus.metricsOps[IO](registry, "prefix")
  } yield Metrics[IO](metrics, classifier)(httpClient)
// prefixedClient: Resource[IO, Client[IO]] = Bind(
//   source = Allocate(
//     resource = cats.effect.kernel.Resource$$$Lambda$22343/829895433@e599290
//   ),
//   fs = <function1>
// )

A classifier is just a function Request[F] => Option[String] that allows to add a label to every metric based on the Request

Examples

Send a GET request, treating the response as a string

You can send a GET by calling the expect method on the client, passing a Uri:

httpClient.expect[String](uri"https://google.com/")
// res5: IO[String] = FlatMap(
//   ioe = Delay(
//     thunk = org.http4s.client.JavaNetClientBuilder$$Lambda$22685/338499742@4a5d0120,
//     event = cats.effect.tracing.TracingEvent$StackTrace
//   ),
//   f = cats.effect.kernel.Resource$$Lambda$22665/239848797@35e80c50,
//   event = cats.effect.tracing.TracingEvent$StackTrace
// )

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 org.http4s.client.dsl.io._
import org.http4s.headers._
import org.http4s.MediaType
val request = GET(
  uri"https://my-lovely-api.com/",
  Authorization(Credentials.Token(AuthScheme.Bearer, "open sesame")),
  Accept(MediaType.application.json)
)
// request: Request[IO] = (
//    = GET,
//    = Uri(
//     scheme = Some(value = Scheme(https)),
//     authority = Some(
//       value = Authority(
//         userInfo = None,
//         host = RegName(host = my-lovely-api.com),
//         port = None
//       )
//     ),
//     path = /,
//     query = ,
//     fragment = None
//   ),
//    = HttpVersion(major = 1, minor = 1),
//    = Headers(Authorization: Bearer open sesame, Accept: application/json),
//    = Entity.Empty,
//    = org.typelevel.vault.Vault@2b123909
// )

httpClient.expect[String](request)
// res6: IO[String] = FlatMap(
//   ioe = Delay(
//     thunk = org.http4s.client.JavaNetClientBuilder$$Lambda$22685/338499742@296b1c99,
//     event = cats.effect.tracing.TracingEvent$StackTrace
//   ),
//   f = cats.effect.kernel.Resource$$Lambda$22665/239848797@3a70dabd,
//   event = cats.effect.tracing.TracingEvent$StackTrace
// )

Post a form, decoding the JSON response to a case class

import org.http4s.circe._
import io.circe.generic.auto._

case class AuthResponse(access_token: String)

implicit val authResponseEntityDecoder: EntityDecoder[IO, AuthResponse] =
  jsonOf[IO, AuthResponse]
// authResponseEntityDecoder: EntityDecoder[IO, AuthResponse] = org.http4s.EntityDecoder$$anon$2@4d4dc9c6

val postRequest = POST(
  UrlForm(
    "grant_type" -> "client_credentials",
    "client_id" -> "my-awesome-client",
    "client_secret" -> "s3cr3t"
  ),
  uri"https://my-lovely-api.com/oauth2/token"
)
// postRequest: Request[IO] = (
//    = POST,
//    = Uri(
//     scheme = Some(value = Scheme(https)),
//     authority = Some(
//       value = Authority(
//         userInfo = None,
//         host = RegName(host = my-lovely-api.com),
//         port = None
//       )
//     ),
//     path = /oauth2/token,
//     query = ,
//     fragment = None
//   ),
//    = HttpVersion(major = 1, minor = 1),
//    = Headers(Content-Type: application/x-www-form-urlencoded; charset=UTF-8, Content-Length: 78),
//    = Strict(
//     chunk = ArraySlice(
//       values = Array(
//         103,
//         114,
//         97,
//         110,
//         116,
//         95,
//         116,
//         121,
//         112,
//         101,
//         61,
//         99,
//         108,
//         105,
//         101,
//         110,
//         116,
//         95,
//         99,
//         114,
//         101,
//         100,
//         101,
//         110,
//         116,
//         105,
//         97,
//         108,
// ...

httpClient.expect[AuthResponse](postRequest)
// res7: IO[AuthResponse] = FlatMap(
//   ioe = Delay(
//     thunk = org.http4s.client.JavaNetClientBuilder$$Lambda$22685/338499742@6533eaf,
//     event = cats.effect.tracing.TracingEvent$StackTrace
//   ),
//   f = cats.effect.kernel.Resource$$Lambda$22665/239848797@3acaa9ea,
//   event = cats.effect.tracing.TracingEvent$StackTrace
// )

Calls to a JSON API

Take a look at json.

Body decoding / encoding

The reusable way to decode/encode a request is to write a custom EntityDecoder and EntityEncoder. For that topic, take a look at entity.

If you prefer a more fine-grained approach, some of the methods take a Response[F] => F[A] argument, such as run or get, which lets you add a function which includes the decoding functionality, but ignores the media type.

client.run(req).use {
  case Status.Successful(r) => r.attemptAs[A].leftMap(_.message).value
  case r => r.as[String]
    .map(b => Left(s"Request $req failed with status ${r.status.code} and body $b"))
}

However, your function has to consume the body before the returned F exits. Don't do this:

// will come back to haunt you
client.get[EntityBody]("some-url")(response => response.body)

Passing it to a EntityDecoder is safe.

client.get[T]("some-url")(response => jsonOf(response.body))