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 Request
s
and Response
s 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))