All Downloads are FREE. Search and download functionalities are using the official Maven repository.

sttp.client4.httpclient.HttpClientAsyncBackend.scala Maven / Gradle / Ivy

The newest version!
package sttp.client4.httpclient

import sttp.capabilities.{Streams, WebSockets}
import sttp.client4.httpclient.HttpClientBackend.EncodingHandler
import sttp.client4.internal.SttpToJavaConverters.{toJavaBiConsumer, toJavaFunction}
import sttp.client4.internal.httpclient.{AddToQueueListener, DelegatingWebSocketListener, Sequencer, WebSocketImpl}
import sttp.client4.internal.ws.{SimpleQueue, WebSocketEvent}
import sttp.client4.{GenericRequest, Response, WebSocketBackend}
import sttp.model.StatusCode
import sttp.monad.syntax._
import sttp.monad.{Canceler, MonadAsyncError}

import java.net.http._
import java.util.concurrent.CompletionException
import java.util.concurrent.atomic.AtomicBoolean
import java.util.function.BiConsumer

/** @tparam F
  *   The effect type
  * @tparam S
  *   Type of supported byte streams, `Nothing` if none
  * @tparam BH
  *   The low-level type of the body, read using a [[HttpResponse.BodyHandler]] read by [[HttpClient]].
  * @tparam B
  *   The higher-level body to which `BH` is transformed (e.g. a backend-native stream representation), which then is
  *   used to read the body as described by `responseAs`.
  */
abstract class HttpClientAsyncBackend[F[_], S <: Streams[S], BH, B](
    client: HttpClient,
    override implicit val monad: MonadAsyncError[F],
    closeClient: Boolean,
    customizeRequest: HttpRequest => HttpRequest,
    customEncodingHandler: EncodingHandler[B]
) extends HttpClientBackend[F, S, S with WebSockets, B](client, closeClient, customEncodingHandler)
    with WebSocketBackend[F] {
  protected def createSimpleQueue[T]: F[SimpleQueue[F, T]]
  protected def createSequencer: F[Sequencer[F]]
  protected def createBodyHandler: HttpResponse.BodyHandler[BH]
  protected def bodyHandlerBodyToBody(p: BH): B
  protected def emptyBody(): B

  override def sendRegular[T](request: GenericRequest[T, R]): F[Response[T]] =
    monad.flatMap(convertRequest(request)) { convertedRequest =>
      val jRequest = customizeRequest(convertedRequest)

      monad.flatten(monad.async[F[Response[T]]] { cb =>
        def success(r: F[Response[T]]): Unit = cb(Right(r))
        def error(t: Throwable): Unit = cb(Left(t))
        var cf = client.sendAsync(jRequest, createBodyHandler)

        val consumer = toJavaBiConsumer { (t: HttpResponse[BH], u: Throwable) =>
          if (t != null) {
            // sometimes body returned by HttpClient can be null, we handle this by returning empty body to prevent NPE
            val body = Option(t.body())
              .map(bodyHandlerBodyToBody)
              .getOrElse(emptyBody())

            try success(readResponse(t, Left(body), request))
            catch {
              case e: Exception => error(e)
            }
          }
          if (u != null) {
            error(u)
          }
        }

        cf = client.executor().orElse(null) match {
          case null => cf.whenComplete(consumer)
          case e    => cf.whenCompleteAsync(consumer, e) // using the provided executor to further process the body
        }

        Canceler(() => cf.cancel(true))
      })
    }

  override def sendWebSocket[T](request: GenericRequest[T, R]): F[Response[T]] =
    (for {
      queue <- createSimpleQueue[WebSocketEvent]
      sequencer <- createSequencer
      ws <- sendWebSocket(request, queue, sequencer)
    } yield ws).handleError {
      case e: CompletionException if e.getCause.isInstanceOf[WebSocketHandshakeException] =>
        readResponse(
          e.getCause.asInstanceOf[WebSocketHandshakeException].getResponse,
          Left(emptyBody()),
          request
        )
    }

  private def sendWebSocket[T](
      request: GenericRequest[T, R],
      queue: SimpleQueue[F, WebSocketEvent],
      sequencer: Sequencer[F]
  ): F[Response[T]] = {
    val isOpen: AtomicBoolean = new AtomicBoolean(false)
    monad.flatten(monad.async[F[Response[T]]] { cb =>
      def success(r: F[Response[T]]): Unit = cb(Right(r))
      def error(t: Throwable): Unit = cb(Left(t))

      val listener = new DelegatingWebSocketListener(
        new AddToQueueListener(queue, isOpen),
        ws => {
          val webSocket = new WebSocketImpl[F](
            ws,
            queue,
            isOpen,
            sequencer,
            monad,
            cf =>
              monad.async { cb =>
                cf.whenComplete(new BiConsumer[WebSocket, Throwable] {
                  override def accept(t: WebSocket, error: Throwable): Unit =
                    if (error != null) {
                      cb(Left(error))
                    } else {
                      cb(Right(()))
                    }
                })
                Canceler { () =>
                  cf.cancel(true)
                  ()
                }
              }
          )
          val baseResponse = Response((), StatusCode.SwitchingProtocols, "", Nil, Nil, request.onlyMetadata)
          val body = bodyFromHttpClient(Right(webSocket), request.response, baseResponse)
          success(body.map(b => baseResponse.copy(body = b)))
        },
        error
      )

      val cf = prepareWebSocketBuilder(request, client)
        .buildAsync(request.uri.toJavaUri, listener)
        .thenApply[Unit](toJavaFunction((_: WebSocket) => ()))
        .exceptionally(toJavaFunction((t: Throwable) => cb(Left(t))))
      Canceler(() => cf.cancel(true))
    })
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy