Build Status Maven Central javadoc

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


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 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`:

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{InetSocketAddress, ProxySelector}
import java.time.Duration

val client0: IO[Client[IO]] = IO.executor.flatMap { exec =>
  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: 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.


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) =
    .map { httpClient =>
      (JdkHttpClient[IO](httpClient), JdkWSClient[IO](httpClient))
// http: Client[IO] = org.http4s.client.Client$$anon$3@5f8c8246
// webSocket: WSClient[IO] = org.http4s.client.websocket.WSClient$$anon$7@1faad830

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


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 =>
    .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.