package org.http4s.client.jdkhttpclient import java.net.URI import java.net.http.HttpRequest.BodyPublishers import java.net.http.HttpResponse.BodyHandlers import java.net.http.{HttpClient, HttpRequest, HttpResponse} import java.nio.ByteBuffer import java.util import java.util.concurrent.Flow import cats.ApplicativeError import cats.effect._ import cats.implicits._ import fs2.concurrent.SignallingRef import fs2.interop.reactivestreams._ import fs2.{Chunk, Stream} import org.http4s.client.Client import org.http4s.client.jdkhttpclient.compat.CollectionConverters._ import org.http4s.internal.fromCompletionStage import org.http4s.util.CaseInsensitiveString import org.http4s.{Header, Headers, HttpVersion, Request, Response, Status} import org.reactivestreams.FlowAdapters object JdkHttpClient { /** * Creates a `Client` from an `HttpClient`. Note that the creation of an `HttpClient` is a * side effect. * * @param jdkHttpClient The `HttpClient`. * @param ignoredHeaders A set of ignored request headers. Some headers (like Content-Length) are * "restricted" and cannot be set by the user. By default, the set of * restricted headers of the OpenJDK 11 is used. */ def apply[F[_]]( jdkHttpClient: HttpClient, ignoredHeaders: Set[CaseInsensitiveString] = restrictedHeaders )(implicit F: ConcurrentEffect[F], CS: ContextShift[F]): Client[F] = { def convertRequest(req: Request[F]): F[HttpRequest] = convertHttpVersionFromHttp4s[F](req.httpVersion).map { version => val rb = HttpRequest.newBuilder .method( req.method.name, { val publisher = FlowAdapters.toFlowPublisher( StreamUnicastPublisher(req.body.chunks.map(_.toByteBuffer)) ) if (req.isChunked) BodyPublishers.fromPublisher(publisher) else req.contentLength .fold(BodyPublishers.noBody)(BodyPublishers.fromPublisher(publisher, _)) } ) .uri(URI.create(req.uri.renderString)) .version(version) val headers = req.headers.iterator .filterNot(h => ignoredHeaders.contains(h.name)) .flatMap(h => Iterator(h.name.value, h.value)) .toArray (if (headers.isEmpty) rb else rb.headers(headers: _*)).build } def convertResponse( res: HttpResponse[Flow.Publisher[util.List[ByteBuffer]]] ): Resource[F, Response[F]] = Resource( (F.fromEither(Status.fromInt(res.statusCode)), SignallingRef[F, Boolean](false)).mapN { case (status, signal) => Response( status = status, headers = Headers(res.headers.map.asScala.flatMap { case (k, vs) => vs.asScala.map(Header(k, _)) }.toList), httpVersion = res.version match { case HttpClient.Version.HTTP_1_1 => HttpVersion.`HTTP/1.1` case HttpClient.Version.HTTP_2 => HttpVersion.`HTTP/2.0` }, body = FlowAdapters .toPublisher(res.body) .toStream[F] .interruptWhen(signal) .flatMap(bs => Stream.fromIterator(bs.iterator.asScala.map(Chunk.byteBuffer))) .flatMap(Stream.chunk) ) -> signal.set(true) } ) Client[F] { req => for { req <- Resource.liftF(convertRequest(req)) res <- Resource.liftF( fromCompletionStage(F.delay(jdkHttpClient.sendAsync(req, BodyHandlers.ofPublisher))) ) res <- convertResponse(res) } yield res } } /** * A `Client` wrapping the default `HttpClient`. */ def simple[F[_]](implicit F: ConcurrentEffect[F], CS: ContextShift[F]): F[Client[F]] = F.delay(HttpClient.newHttpClient()).map(apply(_)) def convertHttpVersionFromHttp4s[F[_]]( version: HttpVersion )(implicit F: ApplicativeError[F, Throwable]): F[HttpClient.Version] = version match { case HttpVersion.`HTTP/1.1` => HttpClient.Version.HTTP_1_1.pure[F] case HttpVersion.`HTTP/2.0` => HttpClient.Version.HTTP_2.pure[F] case _ => F.raiseError(new IllegalArgumentException("invalid HTTP version")) } // see jdk.internal.net.http.common.Utils#DISALLOWED_HEADERS_SET private val restrictedHeaders = Set( "connection", "content-length", "date", "expect", "from", "host", "upgrade", "via", "warning" ).map(CaseInsensitiveString(_)) }