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.11.8" // Also supports 2.10.x and 2.12.x
val http4sVersion = "0.18.26"
// 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 tut picks it up:
import cats.effect._
import org.http4s._
import org.http4s.dsl.io._
import org.http4s.server.blaze._
val service = HttpService[IO] {
case GET -> Root / "hello" / name =>
Ok(s"Hello, $name.")
}
// service: HttpService[IO] = Kleisli(
// org.http4s.HttpService$$$Lambda$12903/708292025@283ac541
// )
val builder = BlazeBuilder[IO].bindHttp(8080, "localhost").mountService(service, "/").start
// builder: IO[org.http4s.server.Server[IO]] = Delay(
// org.http4s.server.blaze.BlazeBuilder$$Lambda$13116/532336336@1f1ae5c5
// )
val server = builder.unsafeRunSync
// server: org.http4s.server.Server[IO] = BlazeServer(/127.0.0.1:8080)
Creating the client
A good default choice is the Http1Client
. The Http1Client
maintains a connection pool and
speaks HTTP 1.x.
Note: In production code you would want to use Http1Client.stream[F[_]: Effect]: Stream[F, Http1Client]
to safely acquire and release resources. In the documentation we are forced to use .unsafeRunSync
to
create the client.
import org.http4s.client.blaze._
val httpClient = Http1Client[IO]().unsafeRunSync
// httpClient: client.Client[IO] = Client(
// Kleisli(
// org.http4s.client.blaze.BlazeClient$$$Lambda$13136/1457536634@62754345
// ),
// Delay(org.http4s.client.PoolManager$$Lambda$13135/909745329@2c101ea7)
// )
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(
// Suspend(
// org.http4s.client.blaze.BlazeClient$$$Lambda$13144/1965911457@527ed6a9
// ),
// org.http4s.client.Client$$Lambda$13145/1564429491@1a8235ee
// )
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
import scala.concurrent.ExecutionContext.Implicits.global
def hello(name: String): IO[String] = {
val target = Uri.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 = fs2.async.parallelTraverse(people)(hello)
// greetingList: IO[Vector[String]] = Bind(
// Map(
// Bind(
// Bind(
// Delay(fs2.async.Promise$$$Lambda$13649/1847638407@52253290),
// fs2.async.package$$$Lambda$13830/1913794827@7eeceb55
// ),
// cats.FlatMap$$Lambda$13689/1211846539@5a03a1bf
// ),
// scala.Function2$$Lambda$11335/1417788686@5125d254,
// 0
// ),
// fs2.async.package$$$Lambda$13831/674374532@2a6fcb52
// )
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
HttpService[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(
// Bind(
// Map(
// Bind(
// Bind(
// Delay(fs2.async.Promise$$$Lambda$13649/1847638407@52253290),
// fs2.async.package$$$Lambda$13830/1913794827@7eeceb55
// ),
// cats.FlatMap$$Lambda$13689/1211846539@5a03a1bf
// ),
// scala.Function2$$Lambda$11335/1417788686@5125d254,
// 0
// ),
// fs2.async.package$$$Lambda$13831/674374532@2a6fcb52
// ),
// <function1>,
// 0
// )
greetingsStringEffect.unsafeRunSync
// res0: 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(...)
:
Uri.uri("https://my-awesome-service.com/foo/bar?wow=yeah")
// res1: Uri = Uri(
// Some(Scheme(https)),
// Some(Authority(None, RegName(my-awesome-service.com), None)),
// "/foo/bar",
// Query(("wow", Some("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",
// Query(("wow", Some("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.uri("http://foo.com")
// baseUri: Uri = Uri(
// Some(Scheme(http)),
// Some(Authority(None, RegName(foo.com), None)),
// "",
// Query(),
// None
// )
val withPath = baseUri.withPath("/bar/baz")
// withPath: Uri = Uri(
// Some(Scheme(http)),
// Some(Authority(None, RegName(foo.com), None)),
// "/bar/baz",
// Query(),
// None
// )
val withQuery = withPath.withQueryParam("hello", "world")
// withQuery: withPath.Self = Uri(
// Some(Scheme(http)),
// Some(Authority(None, RegName(foo.com), None)),
// "/bar/baz",
// Query(("hello", Some("world"))),
// None
// )
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.uri("https://google.com/"))
// res2: IO[String] = Bind(
// Suspend(
// org.http4s.client.blaze.BlazeClient$$$Lambda$13144/1965911457@74897a87
// ),
// org.http4s.client.Client$$Lambda$13145/1564429491@bc1b130
// )
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.uri("https://my-lovely-api.com/"),
Authorization(Credentials.Token(AuthScheme.Bearer, "open sesame")),
Accept(`application/json`)
)
// request: IO[Request[IO]] = Pure(
// Request(
// Method("GET"),
// Uri(
// Some(Scheme(https)),
// Some(Authority(None, RegName(my-lovely-api.com), None)),
// "/",
// Query(),
// None
// ),
// HttpVersion(1, 1),
// Headers(
// Authorization(Token(Bearer, "open sesame")),
// Accept(
// NonEmptyList(
// MediaRangeAndQValue(MediaType(application/json), QValue(1.0)),
// List()
// )
// )
// ),
// Stream(..),
// org.http4s.AttributeMap@8cf85da
// )
// )
httpClient.expect[String](request)
// res3: IO[String] = Bind(
// Pure(
// Request(
// Method("GET"),
// Uri(
// Some(Scheme(https)),
// Some(Authority(None, RegName(my-lovely-api.com), None)),
// "/",
// Query(),
// None
// ),
// HttpVersion(1, 1),
// Headers(
// Authorization(Token(Bearer, "open sesame")),
// Accept(
// NonEmptyList(
// MediaRangeAndQValue(MediaType(application/json), QValue(1.0)),
// List()
// )
// )
// ),
// Stream(..),
// org.http4s.AttributeMap@8cf85da
// )
// ),
// org.http4s.client.Client$$Lambda$13138/188324034@5fa0ce41
// )
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(
Uri.uri("https://my-lovely-api.com/oauth2/token"),
UrlForm(
"grant_type" -> "client_credentials",
"client_id" -> "my-awesome-client",
"client_secret" -> "s3cr3t"
)
)
// postRequest: IO[Request[IO]] = Bind(
// Pure(Entity(Stream(..), Some(78L))),
// org.http4s.client.impl.EntityRequestGenerator$$Lambda$13100/1065693990@6d8a3ecf
// )
httpClient.expect[AuthResponse](postRequest)
// res4: IO[AuthResponse] = Bind(
// Bind(
// Pure(Entity(Stream(..), Some(78L))),
// org.http4s.client.impl.EntityRequestGenerator$$Lambda$13100/1065693990@6d8a3ecf
// ),
// org.http4s.client.Client$$Lambda$13138/188324034@52e82cbb
// )
Cleaning up
Our client consumes system resources. Let’s clean up after ourselves by shutting
it down:
httpClient.shutdownNow()
If the client is created using HttpClient.stream[F]()
, it will be shut down when
the resulting stream finishes.
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 fetch
or get
, which lets you add a function which includes the
decoding functionality, but ignores the media type.
client.fetch(req) {
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))