http4s-jdk-http-client

Build Status Maven Central Scaladoc

HTTP client

http4s-jdk-http-client contains a http4s-client implementation based on the java.net.http.HttpClient introduced in Java 11.

Installation

To use http4s-jdk-http-client in an existing SBT project, add the following dependency to your build.sbt:

libraryDependencies ++= Seq(
  "org.http4s" %% "http4s-jdk-http-client" % "0.2.0-SNAPSHOT"
)

Compatibility

  • Requires Java 11 or greater
  • Built for Scala 2.12 and 2.13
  • Works with http4s-client-0.21.0-M4

Creating the client

Simple

A default JDK HTTP client can be created with a call to simple for any ConcurrentEffect type, such as cats.effect.IO:

import cats.effect.IO
import org.http4s.client.Client
import org.http4s.client.jdkhttpclient.JdkHttpClient

// A `Timer` and `ContextShift` are necessary for a `ConcurrentEffect[IO]`.
// They come for free when you use `cats.effect.IOApp`:
import cats.effect.{ContextShift, Timer}
import scala.concurrent.ExecutionContext.Implicits.global
implicit val timer: cats.effect.Timer[IO] = IO.timer(global)
implicit val cs: cats.effect.ContextShift[IO] = IO.contextShift(global)

val client: IO[Client[IO]] = JdkHttpClient.simple[IO]

Custom clients

A JDK HTTP client can be passed to JdkHttpClient.apply for use as an http4s-client backend. It is a good idea to create the HttpClient in an effect, as it creates a default executor and SSL context:

import java.net.{InetSocketAddress, ProxySelector}
import java.net.http.HttpClient

val client0: IO[Client[IO]] = IO {
  HttpClient.newBuilder()
    .version(HttpClient.Version.HTTP_2)
    .proxy(ProxySelector.of(new InetSocketAddress("www-proxy", 8080)))
    .build()
}.map(JdkHttpClient(_))

Sharing

The client instance contains shared resources such as a connection pool, and should be passed as an argument to code that uses it:

import cats.effect._
import cats.implicits._
import org.http4s._
import org.http4s.implicits._
  
def fetchStatus[F[_]](c: Client[F], uri: Uri): F[Status] =
  c.status(Request[F](Method.GET, uri = uri))

client
  .flatMap(c => fetchStatus(c, uri"https://http4s.org/"))
  .attempt
  .unsafeRunSync()
// res1: Either[Throwable, Status] = Right(Status(200))
Failure to share

Contrast with this alternate definition of fetchStatus, which would create a new HttpClient instance on every invocation:

def fetchStatusInefficiently[F[_]: ConcurrentEffect](uri: Uri): F[Status] =
  JdkHttpClient.simple[F].flatMap(_.status(Request[F](Method.GET, uri = uri)))

Shutdown

Clients created with this back end do not need to be shut down.

Further reading

For more details on the http4s-client, please see the core client documentation.

Websocket client

This package also contains a functional websocket client. Please note that the API may change in the future.

Creation

A WSClient is created using an HttpClient as above. It is encouraged to use the same HttpClient to construct a Client[F] and a WSClient[F].

import org.http4s.client.jdkhttpclient._

val (http, webSocket) =
  IO(HttpClient.newHttpClient())
    .map { httpClient =>
      (JdkHttpClient[IO](httpClient), JdkWSClient[IO](httpClient))
    }
    .unsafeRunSync()
// http: Client[IO] = org.http4s.client.Client$$anon$1@18fa12c0
// webSocket: WSClient[IO] = org.http4s.client.jdkhttpclient.WSClient$$anon$1@4cae2a84

If you do not need an HTTP client, you can also call JdkWSClient.simple[IO] as above.

Overview

We have the following websocket frame hierarchy:

  • WSFrame
  • WSControlFrame
    • WSFrame.Close
    • WSFrame.Ping
    • WSFrame.Pong
  • WSDataFrame
    • WSFrame.Text
    • WSFrame.Binary

There are two connection modes: “low-level” and “high-level”. Both manage the lifetime of a websocket connection via a Resource. In the low-level mode, you can send and have to receive arbitrary WSFrames. The high-level mode does the following things for you:

  • Hides the control frames (you can still send Ping and Close frames).
  • Responds to Ping frames with Pongs and echoes Close frames (the received Close frame is exposed as a TryableDeferred). In fact, this currently also the case for the “low-level” mode, but this will change when other websocket backends are added.
  • Groups the data frames by their last attribute.

Usage example

We use the “high-level” connection mode to build a simple websocket app.

webSocket
  .connectHighLevel(WSRequest(uri"wss://echo.websocket.org"))
  .use { conn =>
    for {
      // send a single Text frame
      _ <- conn.send(WSFrame.Text("reality"))
      // send multiple frames (both Text and Binary are possible)
      // "faster" than individual `send` calls
      _ <- conn.sendMany(List(
        WSFrame.Text("is often"),
        WSFrame.Text("disappointing.")
      ))
      received <- conn
        // a backpressured stream of incoming frames
        .receiveStream
        // we do not care about Binary frames (and will never receive any)
        .collect { case WSFrame.Text(str, _) => str }
        // send back the modified text
        .evalTap(str => conn.send(WSFrame.Text(str.toUpperCase)))
        .take(6)
        .compile
        .toList
    } yield received.mkString(" ")
  } // the connection is closed here
  .unsafeRunSync()
// res2: String = "reality is often disappointing. REALITY IS OFTEN DISAPPOINTING."

For an overview of all options and functions visit the scaladoc.