Build Status Maven Central Scaladoc

HTTP client

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


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.3.1+69-c93be705-SNAPSHOT"


  • Requires Java 11 or greater
  • Built for Scala 2.12 and 2.13
  • Works with http4s-client-0.21.8
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


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}
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{InetSocketAddress, ProxySelector}

val client0: IO[Client[IO]] = IO {
    .proxy(ProxySelector.of(new InetSocketAddress("www-proxy", 8080)))


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

  .flatMap(c => fetchStatus(c, uri""))
// 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: ContextShift](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.


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.


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) =
    .map { httpClient =>
      (JdkHttpClient[IO](httpClient), JdkWSClient[IO](httpClient))
// http: Client[IO] = org.http4s.client.Client$$anon$1@71006060
// webSocket: WSClient[IO] = org.http4s.client.jdkhttpclient.WSClient$$anon$1@7456fd18

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


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.

  .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"),
      received <- conn
        // a backpressured stream of incoming frames
        // 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)))
    } yield received.mkString(" ")
  } // the connection is closed here
// res2: String = "reality is often disappointing. REALITY IS OFTEN DISAPPOINTING."

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