http4s-jdk-http-client

Build Status Maven Central javadoc

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.9.1"
)

Compatibility

TLS 1.3 on Java 11. On Java 11, TLS 1.3 is disabled by default (when using JdkHttpClient.simple). This is a workaround for a spurious bug, see #200.

Creating the client

Simple

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

import cats.effect.{IO, Resource}
import org.http4s.client.Client
import org.http4s.jdkhttpclient.JdkHttpClient

// Here, we import the global runtime.
// It comes for free with `cats.effect.IOApp`:
import cats.effect.unsafe.implicits.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 an SSL context:

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

val client0: IO[Client[IO]] = IO.executor.flatMap { exec =>
  IO {
    HttpClient.newBuilder()
      .version(HttpClient.Version.HTTP_2)
      .proxy(ProxySelector.of(new InetSocketAddress("www-proxy", 8080)))
      .executor(exec)
      .connectTimeout(Duration.ofSeconds(10))
      .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/"))
  .unsafeRunSync()
// res1: Status = Status(code = 200)

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

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

Restricted headers

The underlying HttpClient may disallow certain request headers like Host or Content-Length to be set directly by the user. Therefore, you can pass a set of ignored headers to JdkHttpClient.apply. By default, the set of restricted headers of OpenJDK 11 is used.

In OpenJDK 12+, there are less restricted headers by default, and you can disable the restriction for certain headers by passing -Djdk.httpclient.allowRestrictedHeaders=host,content-length etc. to java.

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.websocket._
import org.http4s.jdkhttpclient._

val (http, webSocket) =
  IO(HttpClient.newHttpClient())
    .map { httpClient =>
      (JdkHttpClient[IO](httpClient), JdkWSClient[IO](httpClient))
    }
    .unsafeRunSync()
// http: Client[IO] = org.http4s.client.Client$$anon$3@9323e6e
// webSocket: WSClient[IO] = org.http4s.client.websocket.WSClient$$anon$7@993ccf4

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:

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:

Usage example

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

echoServer.use { echoUri =>
  webSocket
    .connectHighLevel(WSRequest(echoUri))
    .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.