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.4" // Also supports 2.11.x and 2.12.x

val http4sVersion = "0.21.32"

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

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

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

import cats.effect._
import org.http4s._
import org.http4s.dsl.io._
import org.http4s.implicits._
import org.http4s.server.blaze._

Blaze needs a [ConcurrentEffect] instance, which is derived from [ContextShift]. The following lines are not necessary if you are in an [IOApp]:

import scala.concurrent.ExecutionContext.global
implicit val cs: ContextShift[IO] = IO.contextShift(global)
implicit val timer: Timer[IO] = IO.timer(global)

Finish setting up our server:

import scala.concurrent.ExecutionContext.global

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

val server = BlazeServerBuilder[IO](global).bindHttp(8080, "localhost").withHttpApp(app).resource
// server: Resource[IO, org.http4s.server.Server[IO]] = Bind(
//   Allocate(
//     Map(
//       Delay(org.http4s.blazecore.package$$$Lambda$8369/773210309@42bd9ca9),
//       cats.effect.Resource$$$Lambda$8370/1084988479@274062c9,
//       StackTrace(
//         List(
//           cats.effect.internals.IOTracing$.buildFrame(IOTracing.scala:48),
//           cats.effect.internals.IOTracing$.buildCachedFrame(IOTracing.scala:39),
//           cats.effect.internals.IOTracing$.cached(IOTracing.scala:34),
//           cats.effect.IO.map(IO.scala:106),
//           cats.effect.IOLowPriorityInstances$IOEffect.map(IO.scala:870),
//           cats.effect.IOLowPriorityInstances$IOEffect.map(IO.scala:863),
//           cats.Functor$Ops.map(Functor.scala:233),
//           cats.Functor$Ops.map$(Functor.scala:233),
//           cats.Functor$ToFunctorOps$$anon$4.map(Functor.scala:250),
//           cats.effect.Resource$.apply(Resource.scala:304),
//           org.http4s.blazecore.package$.tickWheelResource(package.scala:24),
//           org.http4s.server.blaze.BlazeServerBuilder.resource(BlazeServerBuilder.scala:391),
//           repl.MdocSession$App.<init>(client.md:49),
//           repl.MdocSession$.app(client.md:3),
//           mdoc.internal.document.DocumentBuilder$$doc$.$anonfun$build$2(DocumentBuilder.scala:89),
//           scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:23),
//           scala.util.DynamicVariable.withValue(DynamicVariable.scala:62),
//           scala.Console$.withErr(Console.scala:196),
//           mdoc.internal.document.DocumentBuilder$$doc$.$anonfun$build$1(DocumentBuilder.scala:89),
//           scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:23),
//           scala.util.DynamicVariable.withValue(DynamicVariable.scala:62),
//           scala.Console$.withOut(Console.scala:167),
//           mdoc.internal.document.DocumentBuilder$$doc$.build(DocumentBuilder.scala:88),
//           mdoc.internal.markdown.MarkdownBuilder$.$anonfun$buildDocument$2(MarkdownBuilder.scala:47),
//           mdoc.internal.markdown.MarkdownBuilder$$anon$1.run(MarkdownBuilder.scala:103)
//         )
//       )
//     )
//   ),
//   org.http4s.server.blaze.BlazeServerBuilder$$Lambda$8371/572626655@4f088c3f
// )

We’ll start the server in the background. The IO.never keeps it running until we cancel the fiber.

val fiber = server.use(_ => IO.never).start.unsafeRunSync()
// fiber: Fiber[IO, Nothing] = Tuple(
//   Bind(
//     Async(
//       cats.effect.internals.IOAsync$$$Lambda$8258/681007058@4c6fa89,
//       false,
//       StackTrace(
//         List(
//           cats.effect.internals.IOTracing$.buildFrame(IOTracing.scala:48),
//           cats.effect.internals.IOTracing$.buildCachedFrame(IOTracing.scala:39),
//           cats.effect.internals.IOTracing$.cached(IOTracing.scala:34),
//           cats.effect.internals.IOAsync$.apply(IOAsync.scala:30),
//           cats.effect.IO$.async(IO.scala:1263),
//           cats.effect.internals.IOFromFuture$.apply(IOFromFuture.scala:37),
//           cats.effect.internals.IOStart$.fiber(IOStart.scala:48),
//           cats.effect.internals.IOStart$.$anonfun$apply$1(IOStart.scala:42),
//           cats.effect.internals.IOStart$.$anonfun$apply$1$adapted(IOStart.scala:29),
//           cats.effect.internals.IORunLoop$RestartCallback.start(IORunLoop.scala:447),
//           cats.effect.internals.IORunLoop$.cats$effect$internals$IORunLoop$$loop(IORunLoop.scala:156),
//           cats.effect.internals.IORunLoop$.$anonfun$suspendAsync$1(IORunLoop.scala:321),
//           cats.effect.internals.IORunLoop$.$anonfun$suspendAsync$1$adapted(IORunLoop.scala:320),
//           cats.effect.internals.IORunLoop$RestartCallback.start(IORunLoop.scala:447),
//           cats.effect.internals.IORunLoop$.cats$effect$internals$IORunLoop$$loop(IORunLoop.scala:156),
//           cats.effect.internals.IORunLoop$.start(IORunLoop.scala:38),
//           cats.effect.IO.unsafeRunAsync(IO.scala:274),
//           cats.effect.internals.IOPlatform$.unsafeResync(IOPlatform.scala:39),
//           cats.effect.IO.unsafeRunTimed(IO.scala:342),
//           cats.effect.IO.unsafeRunSync(IO.scala:256),
//           repl.MdocSession$App.<init>(client.md:56),
//           repl.MdocSession$.app(client.md:3),
//           mdoc.internal.document.DocumentBuilder$$doc$.$anonfun$build$2(DocumentBuilder.scala:89),
//           scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:23),
//           scala.util.DynamicVariable.withValue(DynamicVariable.scala:62),
//           scala.Console$.withErr(Console.scala:196),
//           mdoc.internal.document.DocumentBuilder$$doc$.$anonfun$build$1(DocumentBuilder.scala:89),
//           scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:23),
//           scala.util.DynamicVariable.withValue(DynamicVariable.scala:62),
//           scala.Console$.withOut(Console.scala:167),
//           mdoc.internal.document.DocumentBuilder$$doc$.build(DocumentBuilder.sca...

Creating the client

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

import org.http4s.client.blaze._
import org.http4s.client._
import scala.concurrent.ExecutionContext.global
BlazeClientBuilder[IO](global).resource.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 blaze client, it does not need to be shut down. Like the blaze-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 cats.effect.Blocker
import java.util.concurrent._

val blockingPool = Executors.newFixedThreadPool(5)
val blocker = Blocker.liftExecutorService(blockingPool)
val httpClient: Client[IO] = JavaNetClientBuilder[IO](blocker).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] = Bind(
//   Map(
//     Delay(org.http4s.client.JavaNetClientBuilder$$Lambda$8448/96625665@725864c0),
//     cats.effect.Resource$$$Lambda$8405/1426276157@41f9dab0,
//     StackTrace(
//       List(
//         cats.effect.internals.IOTracing$.buildFrame(IOTracing.scala:48),
//         cats.effect.internals.IOTracing$.buildCachedFrame(IOTracing.scala:39),
//         cats.effect.internals.IOTracing$.cached(IOTracing.scala:34),
//         cats.effect.IO.map(IO.scala:106),
//         cats.effect.IOLowPriorityInstances$IOEffect.map(IO.scala:870),
//         cats.effect.IOLowPriorityInstances$IOEffect.map(IO.scala:863),
//         cats.Functor$Ops.map(Functor.scala:233),
//         cats.Functor$Ops.map$(Functor.scala:233),
//         cats.Functor$ToFunctorOps$$anon$4.map(Functor.scala:250),
//         cats.effect.Resource$.eval(Resource.scala:375),
//         org.http4s.server.blaze.BlazeServerBuilder.$anonfun$resource$1(BlazeServerBuilder.scala:434),
//         cats.effect.Resource.$anonfun$fold$1(Resource.scala:124),
//         cats.effect.internals.IOBracket$BracketStart.liftedTree1$1(IOBracket.scala:95),
//         cats.effect.internals.IOBracket$BracketStart.run(IOBracket.scala:95),
//         cats.effect.internals.Trampoline.cats$effect$internals$Trampoline$$immediateLoop(Trampoline.scala:67),
//         cats.effect.internals.Trampoline.startLoop(Trampoline.scala:35),
//         cats.effect.internals.TrampolineEC$JVMTrampoline.super$startLoop(TrampolineEC.scala:90),
//         cats.effect.internals.TrampolineEC$JVMTrampoline.$anonfun$startLoop$1(TrampolineEC.scala:90),
//         scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:23),
//         scala.concurrent.BlockContext$.withBlockContext(BlockContext.scala:85),
//         cats.effect.internals.TrampolineEC$JVMTrampoline.startLoop(TrampolineEC.scala:90),
//         cats.effect.internals.Trampoline.execute(Trampoline.scala:43),
//         cats.effect.internals.TrampolineEC.execute(TrampolineEC.scala:42),
//         cats.effect.internals.IOBracket$BracketStart.apply(IOBracket.scala:80),
//         cats.effect.internals.IOBracket$BracketStart.apply(IOBracket.scala:58),
//         cats.effect.internals.IORunLoop$.cats$effect$internals$IORunLoop$$loop(IORunLoop.scala:183),
//         cats.effect.internals.IORunLoop$.restart(IORunLoop.scala:41),
//         cats.effect.internals.IOBracket$.$anonfun$apply$1(IOBracket.scala:48),
//         cats.effect.internals.IOBracket$.$anonfun$apply$1$adapted(IOBracket.scala:34),
//         cats.effect.internals.IOAsync$.$anonfun$apply$1(IOAsync.scala:37),
//         cats.effect.internals.IOAsync$.$anonfun$apply$1$adapted(IOAsync.scala:37),
//         cats.effect.internals.IORunLoop$RestartCallback.start(IORunLoop.scala:447),
// ...

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(
//   Map(
//     Async(<function3>, true, null),
//     cats.data.Chain$$$Lambda$8462/1215472693@6eec7c1d,
//     StackTrace(
//       List(
//         cats.effect.internals.IOTracing$.buildFrame(IOTracing.scala:48),
//         cats.effect.internals.IOTracing$.buildCachedFrame(IOTracing.scala:39),
//         cats.effect.internals.IOTracing$.cached(IOTracing.scala:34),
//         cats.effect.IO.map(IO.scala:106),
//         cats.effect.IOParallelNewtype$$anon$2.map(IO.scala:845),
//         cats.data.Chain$.$anonfun$traverseViaChain$5(Chain.scala:806),
//         cats.Eval.$anonfun$map$1(Eval.scala:57),
//         cats.Eval$.loop$1(Eval.scala:358),
//         cats.Eval$.cats$Eval$$evaluate(Eval.scala:363),
//         cats.Eval$FlatMap.value(Eval.scala:284),
//         cats.data.Chain$.traverseViaChain(Chain.scala:825),
//         cats.instances.VectorInstances$$anon$1.traverse(vector.scala:96),
//         cats.instances.VectorInstances$$anon$1.traverse(vector.scala:15),
//         cats.Parallel$.parTraverse(Parallel.scala:215),
//         cats.syntax.ParallelTraversableOps1$.parTraverse$extension(parallel.scala:135),
//         repl.MdocSession$App.<init>(client.md:128),
//         repl.MdocSession$.app(client.md:3),
//         mdoc.internal.document.DocumentBuilder$$doc$.$anonfun$build$2(DocumentBuilder.scala:89),
//         scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:23),
//         scala.util.DynamicVariable.withValue(DynamicVariable.scala:62),
//         scala.Console$.withErr(Console.scala:196),
//         mdoc.internal.document.DocumentBuilder$$doc$.$anonfun$build$1(DocumentBuilder.scala:89),
//         scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:23),
//         scala.util.DynamicVariable.withValue(DynamicVariable.scala:62),
//         scala.Console$.withOut(Console.scala:167),
//         mdoc.internal.document.DocumentBuilder$$doc$.build(DocumentBuilder.scala:88),
//         mdoc.internal.markdown.MarkdownBuilder$.$anonfun$buildDocument$2(MarkdownBuilder.scala:47),
//         mdoc.internal.markdown.MarkdownBuilder$$anon$1.run(MarkdownBuilder.scala:103)
//       )
//     )
//   ),
//   cats.instances.VectorInstances$$anon$1$$Lambda$8463/378008659@42d82f94,
//   StackTrace(
//     List(
//       cats.effect.internals.IOTracing$.buildFrame(IOTracing.scala:48),
//       cats.effect.internals.IOTracing$.buildCachedFrame(IOTracing.scala:39),
//       cats.effect.internals.IOTracing$.cached(IOTracing.scala:34),
// ...

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:

  • In a command line app, it’s your main method.
  • In an HttpApp[F], an F[Response[F]] is returned to be run by the server.
  • Here in the REPL, the last line is the end of the world. Here we go:
val greetingsStringEffect = greetingList.map(_.mkString("\n"))
// greetingsStringEffect: IO[String] = Map(
//   Map(
//     Map(
//       Async(<function3>, true, null),
//       cats.data.Chain$$$Lambda$8462/1215472693@6eec7c1d,
//       StackTrace(
//         List(
//           cats.effect.internals.IOTracing$.buildFrame(IOTracing.scala:48),
//           cats.effect.internals.IOTracing$.buildCachedFrame(IOTracing.scala:39),
//           cats.effect.internals.IOTracing$.cached(IOTracing.scala:34),
//           cats.effect.IO.map(IO.scala:106),
//           cats.effect.IOParallelNewtype$$anon$2.map(IO.scala:845),
//           cats.data.Chain$.$anonfun$traverseViaChain$5(Chain.scala:806),
//           cats.Eval.$anonfun$map$1(Eval.scala:57),
//           cats.Eval$.loop$1(Eval.scala:358),
//           cats.Eval$.cats$Eval$$evaluate(Eval.scala:363),
//           cats.Eval$FlatMap.value(Eval.scala:284),
//           cats.data.Chain$.traverseViaChain(Chain.scala:825),
//           cats.instances.VectorInstances$$anon$1.traverse(vector.scala:96),
//           cats.instances.VectorInstances$$anon$1.traverse(vector.scala:15),
//           cats.Parallel$.parTraverse(Parallel.scala:215),
//           cats.syntax.ParallelTraversableOps1$.parTraverse$extension(parallel.scala:135),
//           repl.MdocSession$App.<init>(client.md:128),
//           repl.MdocSession$.app(client.md:3),
//           mdoc.internal.document.DocumentBuilder$$doc$.$anonfun$build$2(DocumentBuilder.scala:89),
//           scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:23),
//           scala.util.DynamicVariable.withValue(DynamicVariable.scala:62),
//           scala.Console$.withErr(Console.scala:196),
//           mdoc.internal.document.DocumentBuilder$$doc$.$anonfun$build$1(DocumentBuilder.scala:89),
//           scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:23),
//           scala.util.DynamicVariable.withValue(DynamicVariable.scala:62),
//           scala.Console$.withOut(Console.scala:167),
//           mdoc.internal.document.DocumentBuilder$$doc$.build(DocumentBuilder.scala:88),
//           mdoc.internal.markdown.MarkdownBuilder$.$anonfun$buildDocument$2(MarkdownBuilder.scala:47),
//           mdoc.internal.markdown.MarkdownBuilder$$anon$1.run(MarkdownBuilder.scala:103)
//         )
//       )
//     ),
//     cats.instances.VectorInstances$$anon$1$$Lambda$8463/378008659@42d82f94,
//     StackTrace(
//       List(
// ...
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(
//   Some(Scheme(https)),
//   Some(Authority(None, RegName(my-awesome-service.com), None)),
//   "/foo/bar",
//   wow=yeah,
//   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(
//   Uri(
//     Some(Scheme(https)),
//     Some(Authority(None, RegName(my-awesome-service.com), None)),
//     "/foo/bar",
//     wow=yeah,
//     None
//   )
// )

val parseFailure: Either[ParseFailure, Uri] = Uri.fromString(invalidUri)
// parseFailure: Either[ParseFailure, Uri] = Left(
//   ParseFailure(
//     "Invalid URI",
//     """Invalid input ' ', expected Alpha, Digit, '+', '-', '.', ':', Unreserved, PctEncoded or SubDelims (line 1, column 5):
// yeah whatever
//     ^"""
//   )
// )

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

val baseUri = uri"http://foo.com"
// baseUri: Uri = Uri(
//   Some(Scheme(http)),
//   Some(Authority(None, RegName(foo.com), None)),
//   "",
//   ,
//   None
// )
val withPath = baseUri.withPath("/bar/baz")
// withPath: Uri = Uri(
//   Some(Scheme(http)),
//   Some(Authority(None, RegName(foo.com), None)),
//   "/bar/baz",
//   ,
//   None
// )
val withQuery = withPath.withQueryParam("hello", "world")
// withQuery: withPath.Self = Uri(
//   Some(Scheme(http)),
//   Some(Authority(None, RegName(foo.com), None)),
//   "/bar/baz",
//   hello=world,
//   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.

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
implicit val clock = Clock.create[IO]
// clock: Clock[IO] = cats.effect.Clock$$anon$1@70930637
val registry = SharedMetricRegistries.getOrCreate("default")
// registry: com.codahale.metrics.MetricRegistry = com.codahale.metrics.MetricRegistry@60cbbebc
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$1@713d1ecf

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
implicit val clock = Clock.create[IO]
// clock: Clock[IO] = cats.effect.Clock$$anon$1@5ae9b3e1
val requestMethodClassifier = (r: Request[IO]) => Some(r.method.toString.toLowerCase)
// requestMethodClassifier: Request[IO] => Some[String] = <function1>

val meteredClient: Resource[IO, Client[IO]] =
  for {
    registry <- Prometheus.collectorRegistry[IO]
    metrics <- Prometheus.metricsOps[IO](registry, "prefix")
  } yield Metrics[IO](metrics, requestMethodClassifier)(httpClient)
// meteredClient: Resource[IO, Client[IO]] = Bind(
//   Allocate(
//     Map(
//       Map(
//         Delay(
//           org.http4s.metrics.prometheus.Prometheus$$$Lambda$8894/2038568608@32a3a435
//         ),
//         cats.effect.Resource$$$Lambda$8403/897991529@f20d283,
//         StackTrace(
//           List(
//             cats.effect.internals.IOTracing$.buildFrame(IOTracing.scala:48),
//             cats.effect.internals.IOTracing$.buildCachedFrame(IOTracing.scala:39),
//             cats.effect.internals.IOTracing$.cached(IOTracing.scala:34),
//             cats.effect.IO.map(IO.scala:106),
//             cats.effect.IOLowPriorityInstances$IOEffect.map(IO.scala:870),
//             cats.effect.IOLowPriorityInstances$IOEffect.map(IO.scala:863),
//             cats.Functor$Ops.map(Functor.scala:233),
//             cats.Functor$Ops.map$(Functor.scala:233),
//             cats.Functor$ToFunctorOps$$anon$4.map(Functor.scala:250),
//             cats.effect.Resource$.make(Resource.scala:342),
//             org.http4s.server.blaze.BlazeServerBuilder.$anonfun$resource$1(BlazeServerBuilder.scala:412),
//             cats.effect.Resource.$anonfun$fold$1(Resource.scala:124),
//             cats.effect.internals.IOBracket$BracketStart.liftedTree1$1(IOBracket.scala:95),
//             cats.effect.internals.IOBracket$BracketStart.run(IOBracket.scala:95),
//             cats.effect.internals.Trampoline.cats$effect$internals$Trampoline$$immediateLoop(Trampoline.scala:67),
//             cats.effect.internals.Trampoline.startLoop(Trampoline.scala:35),
//             cats.effect.internals.TrampolineEC$JVMTrampoline.super$startLoop(TrampolineEC.scala:90),
//             cats.effect.internals.TrampolineEC$JVMTrampoline.$anonfun$startLoop$1(TrampolineEC.scala:90),
//             scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:23),
//             scala.concurrent.BlockContext$.withBlockContext(BlockContext.scala:85),
//             cats.effect.internals.TrampolineEC$JVMTrampoline.startLoop(TrampolineEC.scala:90),
//             cats.effect.internals.Trampoline.execute(Trampoline.scala:43),
//             cats.effect.internals.TrampolineEC.execute(TrampolineEC.scala:42),
//             cats.effect.internals.IOBracket$BracketStart.apply(IOBracket.scala:80),
//             cats.effect.internals.IOBracket$BracketStart.apply(IOBracket.scala:58),
//             cats.effect.internals.IORunLoop$.cats$effect$internals$IORunLoop$$lo...

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/")
// res3: IO[String] = Bind(
//   Map(
//     Delay(org.http4s.client.JavaNetClientBuilder$$Lambda$8448/96625665@16f27ece),
//     cats.effect.Resource$$$Lambda$8405/1426276157@193b14a,
//     StackTrace(
//       List(
//         cats.effect.internals.IOTracing$.buildFrame(IOTracing.scala:48),
//         cats.effect.internals.IOTracing$.buildCachedFrame(IOTracing.scala:39),
//         cats.effect.internals.IOTracing$.cached(IOTracing.scala:34),
//         cats.effect.IO.map(IO.scala:106),
//         cats.effect.IOLowPriorityInstances$IOEffect.map(IO.scala:870),
//         cats.effect.IOLowPriorityInstances$IOEffect.map(IO.scala:863),
//         cats.Functor$Ops.map(Functor.scala:233),
//         cats.Functor$Ops.map$(Functor.scala:233),
//         cats.Functor$ToFunctorOps$$anon$4.map(Functor.scala:250),
//         cats.effect.Resource$.eval(Resource.scala:375),
//         org.http4s.server.blaze.BlazeServerBuilder.$anonfun$resource$1(BlazeServerBuilder.scala:434),
//         cats.effect.Resource.$anonfun$fold$1(Resource.scala:124),
//         cats.effect.internals.IOBracket$BracketStart.liftedTree1$1(IOBracket.scala:95),
//         cats.effect.internals.IOBracket$BracketStart.run(IOBracket.scala:95),
//         cats.effect.internals.Trampoline.cats$effect$internals$Trampoline$$immediateLoop(Trampoline.scala:67),
//         cats.effect.internals.Trampoline.startLoop(Trampoline.scala:35),
//         cats.effect.internals.TrampolineEC$JVMTrampoline.super$startLoop(TrampolineEC.scala:90),
//         cats.effect.internals.TrampolineEC$JVMTrampoline.$anonfun$startLoop$1(TrampolineEC.scala:90),
//         scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:23),
//         scala.concurrent.BlockContext$.withBlockContext(BlockContext.scala:85),
//         cats.effect.internals.TrampolineEC$JVMTrampoline.startLoop(TrampolineEC.scala:90),
//         cats.effect.internals.Trampoline.execute(Trampoline.scala:43),
//         cats.effect.internals.TrampolineEC.execute(TrampolineEC.scala:42),
//         cats.effect.internals.IOBracket$BracketStart.apply(IOBracket.scala:80),
//         cats.effect.internals.IOBracket$BracketStart.apply(IOBracket.scala:58),
//         cats.effect.internals.IORunLoop$.cats$effect$internals$IORunLoop$$loop(IORunLoop.scala:183),
//         cats.effect.internals.IORunLoop$.restart(IORunLoop.scala:41),
//         cats.effect.internals.IOBracket$.$anonfun$apply$1(IOBracket.scala:48),
//         cats.effect.internals.IOBracket$.$anonfun$apply$1$adapted(IOBracket.scala:34),
//         cats.effect.internals.IOAsync$.$anonfun$apply$1(IOAsync.scala:37),
//         cats.effect.internals.IOAsync$.$anonfun$apply$1$adapted(IOAsync.scala:37),
//         cats.effect.internals.IORunLoop$RestartCallback.start(IORunLoop.scala:447),
// ...

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
import org.http4s.Method._
val request = GET(
  uri"https://my-lovely-api.com/",
  Authorization(Credentials.Token(AuthScheme.Bearer, "open sesame")),
  Accept(MediaType.application.json)
)
// request: IO[Request[IO]] = Pure(
//   (
//     Method("GET"),
//     Uri(
//       Some(Scheme(https)),
//       Some(Authority(None, RegName(my-lovely-api.com), None)),
//       "/",
//       ,
//       None
//     ),
//     HttpVersion(1, 1),
//     Headers(Authorization: Bearer open sesame, Accept: application/json),
//     Stream(..),
//     io.chrisdavenport.vault.Vault@6fd58721
//   )
// )

httpClient.expect[String](request)
// res4: IO[String] = Bind(
//   Pure(
//     (
//       Method("GET"),
//       Uri(
//         Some(Scheme(https)),
//         Some(Authority(None, RegName(my-lovely-api.com), None)),
//         "/",
//         ,
//         None
//       ),
//       HttpVersion(1, 1),
//       Headers(Authorization: Bearer open sesame, Accept: application/json),
//       Stream(..),
//       io.chrisdavenport.vault.Vault@6fd58721
//     )
//   ),
//   org.http4s.client.DefaultClient$$Lambda$8897/974978181@685b512b,
//   StackTrace(
//     List(
//       cats.effect.internals.IOTracing$.buildFrame(IOTracing.scala:48),
//       cats.effect.internals.IOTracing$.buildCachedFrame(IOTracing.scala:39),
//       cats.effect.internals.IOTracing$.cached(IOTracing.scala:34),
//       cats.effect.IO.flatMap(IO.scala:133),
//       cats.effect.IOLowPriorityInstances$IOEffect.flatMap(IO.scala:886),
//       cats.effect.IOLowPriorityInstances$IOEffect.flatMap(IO.scala:863),
//       cats.FlatMap$Ops.flatMap(FlatMap.scala:229),
//       cats.FlatMap$Ops.flatMap$(FlatMap.scala:229),
//       cats.FlatMap$ToFlatMapOps$$anon$2.flatMap(FlatMap.scala:243),
//       org.http4s.client.DefaultClient.expectOr(DefaultClient.scala:127),
//       org.http4s.client.DefaultClient.expect(DefaultClient.scala:130),
//       repl.MdocSession$App.<init>(client.md:267),
//       repl.MdocSession$.app(client.md:3),
//       mdoc.internal.document.DocumentBuilder$$doc$.$anonfun$build$2(DocumentBuilder.scala:89),
//       scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:23),
//       scala.util.DynamicVariable.withValue(DynamicVariable.scala:62),
//       scala.Console$.withErr(Console.scala:196),
//       mdoc.internal.document.DocumentBuilder$$doc$.$anonfun$build$1(DocumentBuilder.scala:89),
//       scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:23),
//       scala.util.DynamicVariable.withValue(DynamicVariable.scala:62),
//       scala.Console$.withOut(Console.scala:167),
//       mdoc.internal.document.DocumentBuilder$$doc$.build(DocumentBuilder.scala:88),
//       mdoc.internal.markdown.MarkdownBuilder$.$anonfun$buildDocument$2(MarkdownBuilder.scala:47),
//       mdoc.internal.markdown.MarkdownBuilder$$anon$1.run(MarkdownBuilder.scala:103)
// ...

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

case class AuthResponse(access_token: String)

// See the JSON page for details on how to define this
implicit val authResponseEntityDecoder: EntityDecoder[IO, AuthResponse] = null
// authResponseEntityDecoder: EntityDecoder[IO, AuthResponse] = null

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: IO[Request[IO]] = Pure(
//   (
//     Method("POST"),
//     Uri(
//       Some(Scheme(https)),
//       Some(Authority(None, RegName(my-lovely-api.com), None)),
//       "/oauth2/token",
//       ,
//       None
//     ),
//     HttpVersion(1, 1),
//     Headers(Content-Type: application/x-www-form-urlencoded; charset=UTF-8, Content-Length: 78),
//     Stream(..),
//     io.chrisdavenport.vault.Vault@121aa015
//   )
// )

httpClient.expect[AuthResponse](postRequest)
// res5: IO[AuthResponse] = Bind(
//   Pure(
//     (
//       Method("POST"),
//       Uri(
//         Some(Scheme(https)),
//         Some(Authority(None, RegName(my-lovely-api.com), None)),
//         "/oauth2/token",
//         ,
//         None
//       ),
//       HttpVersion(1, 1),
//       Headers(Content-Type: application/x-www-form-urlencoded; charset=UTF-8, Content-Length: 78),
//       Stream(..),
//       io.chrisdavenport.vault.Vault@121aa015
//     )
//   ),
//   org.http4s.client.DefaultClient$$Lambda$8897/974978181@73c3d9aa,
//   StackTrace(
//     List(
//       cats.effect.internals.IOTracing$.buildFrame(IOTracing.scala:48),
//       cats.effect.internals.IOTracing$.buildCachedFrame(IOTracing.scala:39),
//       cats.effect.internals.IOTracing$.cached(IOTracing.scala:34),
//       cats.effect.IO.flatMap(IO.scala:133),
//       cats.effect.IOLowPriorityInstances$IOEffect.flatMap(IO.scala:886),
//       cats.effect.IOLowPriorityInstances$IOEffect.flatMap(IO.scala:863),
//       cats.FlatMap$Ops.flatMap(FlatMap.scala:229),
//       cats.FlatMap$Ops.flatMap$(FlatMap.scala:229),
//       cats.FlatMap$ToFlatMapOps$$anon$2.flatMap(FlatMap.scala:243),
//       org.http4s.client.DefaultClient.expectOr(DefaultClient.scala:127),
//       org.http4s.client.DefaultClient.expect(DefaultClient.scala:130),
//       repl.MdocSession$App.<init>(client.md:267),
//       repl.MdocSession$.app(client.md:3),
//       mdoc.internal.document.DocumentBuilder$$doc$.$anonfun$build$2(DocumentBuilder.scala:89),
//       scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:23),
//       scala.util.DynamicVariable.withValue(DynamicVariable.scala:62),
//       scala.Console$.withErr(Console.scala:196),
//       mdoc.internal.document.DocumentBuilder$$doc$.$anonfun$build$1(DocumentBuilder.scala:89),
//       scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:23),
//       scala.util.DynamicVariable.withValue(DynamicVariable.scala:62),
//       scala.Console$.withOut(Console.scala:167),
//       mdoc.internal.document.DocumentBuilder$$doc$.build(DocumentBuilder.scala:88),
//       mdoc.internal.markdown.MarkdownBuilder$.$anonfun$buildDocument$2(MarkdownBuilder.scala:47),
//       mdoc.internal.markdown.MarkdownBuilder$$anon$1.run(MarkdownBuilder.scala:1...

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))