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