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

sttp.client4.okhttp.OkHttpBackend.scala Maven / Gradle / Ivy

There is a newer version: 4.0.0-M19
Show newest version
package sttp.client4.okhttp

import java.io.InputStream
import java.util.concurrent.TimeUnit
import okhttp3.internal.http.HttpMethod
import okhttp3.{
  Authenticator,
  Credentials,
  OkHttpClient,
  Request => OkHttpRequest,
  RequestBody => OkHttpRequestBody,
  Response => OkHttpResponse,
  Route
}
import sttp.capabilities.{Effect, Streams}
import sttp.client4.BackendOptions.Proxy
import sttp.client4.SttpClientException.ReadException
import sttp.client4.internal.ws.SimpleQueue
import sttp.client4._
import sttp.model._

import scala.collection.JavaConverters._
import sttp.client4.compression.Compressor
import sttp.client4.compression.CompressionHandlers
import sttp.client4.compression.Decompressor

abstract class OkHttpBackend[F[_], S <: Streams[S], P](
    client: OkHttpClient,
    closeClient: Boolean,
    compressionHandlers: CompressionHandlers[P, InputStream]
) extends GenericBackend[F, P]
    with Backend[F] {

  val streams: Streams[S]
  type R = P with Effect[F]

  override def send[T](request: GenericRequest[T, R]): F[Response[T]] =
    adjustExceptions(request.isWebSocket, request) {
      if (request.isWebSocket) {
        sendWebSocket(request)
      } else {
        sendRegular(request)
      }
    }

  protected def sendRegular[T](request: GenericRequest[T, R]): F[Response[T]]
  protected def sendWebSocket[T](request: GenericRequest[T, R]): F[Response[T]]

  private def adjustExceptions[T](isWebsocket: Boolean, request: GenericRequest[_, _])(t: => F[T]): F[T] =
    SttpClientException.adjustExceptions(monad)(t)(
      OkHttpBackend.exceptionToSttpClientException(isWebsocket, request, _)
    )

  private[okhttp] def convertRequest[T](request: GenericRequest[T, R]): OkHttpRequest = {
    val builder = new OkHttpRequest.Builder()
      .url(request.uri.toString)

    val (maybeCompressedBody, contentLength) = Compressor.compressIfNeeded(request, compressionHandlers.compressors)
    val body = bodyToOkHttp(maybeCompressedBody, request.contentType, contentLength)
    builder.method(
      request.method.method,
      body.getOrElse {
        if (HttpMethod.requiresRequestBody(request.method.method))
          OkHttpRequestBody.create("", null)
        else null
      }
    )

    // the content-length header's value might have changed due to compression
    request.headers.foreach(header =>
      if (!header.is(HeaderNames.ContentLength)) {
        val _ = builder.addHeader(header.name, header.value)
      }
    )
    contentLength.foreach(cl => builder.addHeader(HeaderNames.ContentLength, cl.toString))

    builder.build()
  }

  protected val bodyToOkHttp: BodyToOkHttp[F, S]
  protected val bodyFromOkHttp: BodyFromOkHttp[F, S]

  private[okhttp] def readResponse[T](
      res: OkHttpResponse,
      request: GenericRequest[_, R],
      responseAs: ResponseAsDelegate[T, R]
  ): F[Response[T]] = {
    val headers = readHeaders(res)
    val responseMetadata = ResponseMetadata(StatusCode(res.code()), res.message(), headers)
    val encoding = headers.collectFirst { case h if h.is(HeaderNames.ContentEncoding) => h.value }
    val method = Method(res.request().method())
    val byteBody =
      if (
        method != Method.HEAD && !res
          .code()
          .equals(StatusCode.NoContent.code) && request.autoDecompressionEnabled
      ) {
        encoding
          .filterNot(_.isEmpty)
          .map(e => Decompressor.decompressIfPossible(res.body().byteStream(), e, compressionHandlers.decompressors))
          .getOrElse(res.body().byteStream())
      } else {
        res.body().byteStream()
      }

    val body = bodyFromOkHttp(byteBody, responseAs, responseMetadata, None)
    monad.map(body)(Response(_, StatusCode(res.code()), res.message(), headers, Nil, request.onlyMetadata))
  }

  private def readHeaders(res: OkHttpResponse): List[Header] =
    res
      .headers()
      .names()
      .asScala
      .flatMap(name => res.headers().values(name).asScala.map(Header(name, _)))
      .toList

  override def close(): F[Unit] =
    if (closeClient) {
      monad.eval(client.dispatcher().executorService().shutdown())
    } else monad.unit(())

  protected def createSimpleQueue[T]: F[SimpleQueue[F, T]]

}

object OkHttpBackend {
  val DefaultWebSocketBufferCapacity: Option[Int] = Some(1024)

  private class ProxyAuthenticator(auth: BackendOptions.ProxyAuth) extends Authenticator {
    override def authenticate(route: Route, response: OkHttpResponse): OkHttpRequest = {
      val credential = Credentials.basic(auth.username, auth.password)
      response.request.newBuilder.header("Proxy-Authorization", credential).build
    }
  }

  private[okhttp] def defaultClient(readTimeout: Long, options: BackendOptions): OkHttpClient = {
    var clientBuilder = new OkHttpClient.Builder()
      .followRedirects(false)
      .followSslRedirects(false)
      .connectTimeout(options.connectionTimeout.toMillis, TimeUnit.MILLISECONDS)
      .readTimeout(readTimeout, TimeUnit.MILLISECONDS)

    clientBuilder = options.proxy match {
      case None => clientBuilder
      case Some(p @ Proxy(_, _, _, _, Some(auth), _)) =>
        clientBuilder.proxySelector(p.asJavaProxySelector).proxyAuthenticator(new ProxyAuthenticator(auth))
      case Some(p) => clientBuilder.proxySelector(p.asJavaProxySelector)
    }

    clientBuilder.build()
  }

  private[okhttp] def updateClientIfCustomReadTimeout[T, S](
      r: GenericRequest[T, S],
      client: OkHttpClient
  ): OkHttpClient = {
    val readTimeoutMillis = if (r.options.readTimeout.isFinite) r.options.readTimeout.toMillis else 0
    val reuseClient = readTimeoutMillis == client.readTimeoutMillis()
    if (reuseClient) client
    else
      client
        .newBuilder()
        .readTimeout(readTimeoutMillis, TimeUnit.MILLISECONDS)
        .build()
  }

  private[okhttp] def exceptionToSttpClientException(
      isWebsocket: Boolean,
      request: GenericRequest[_, _],
      e: Exception
  ): Option[Exception] =
    e match {
      // if the websocket protocol upgrade fails, OkHttp throws a ProtocolException - however the whole request has
      // been already sent, so this is not a TCP-level connect exception
      case e: java.net.ProtocolException if isWebsocket => Some(new ReadException(request, e))
      case e => SttpClientException.defaultExceptionToSttpClientException(request, e)
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy