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

com.apollographql.apollo3.internal.interceptor.ApolloServerInterceptor.kt Maven / Gradle / Ivy

package com.apollographql.apollo3.internal.interceptor

import com.apollographql.apollo3.api.ApolloRequest
import com.apollographql.apollo3.api.Operation
import com.apollographql.apollo3.api.Query
import com.apollographql.apollo3.api.CustomScalarAdapters
import com.apollographql.apollo3.api.cache.http.HttpCache
import com.apollographql.apollo3.api.cache.http.HttpCachePolicy
import com.apollographql.apollo3.api.http.DefaultHttpRequestComposer
import com.apollographql.apollo3.api.http.HttpRequestComposerParams
import com.apollographql.apollo3.api.http.HttpMethod
import com.apollographql.apollo3.api.internal.ApolloLogger
import com.apollographql.apollo3.api.internal.json.BufferedSinkJsonWriter
import com.apollographql.apollo3.cache.ApolloCacheHeaders
import com.apollographql.apollo3.cache.CacheHeaders
import com.apollographql.apollo3.exception.ApolloNetworkException
import com.apollographql.apollo3.interceptor.ApolloInterceptor
import com.apollographql.apollo3.interceptor.ApolloInterceptor.CallBack
import com.apollographql.apollo3.interceptor.ApolloInterceptor.FetchSourceType
import com.apollographql.apollo3.interceptor.ApolloInterceptor.InterceptorRequest
import com.apollographql.apollo3.interceptor.ApolloInterceptor.InterceptorResponse
import com.apollographql.apollo3.interceptor.ApolloInterceptorChain
import com.apollographql.apollo3.request.RequestHeaders
import okhttp3.Call
import okhttp3.Callback
import okhttp3.HttpUrl
import okhttp3.MediaType
import okhttp3.Request
import okhttp3.RequestBody
import okhttp3.Response
import okio.Buffer
import okio.BufferedSink
import java.io.IOException
import java.util.concurrent.Executor
import java.util.concurrent.atomic.AtomicReference

/**
 * ApolloServerInterceptor is a concrete [ApolloInterceptor] responsible for making the network calls to the
 * server. It is the last interceptor in the chain of interceptors and hence doesn't call
 * [ApolloInterceptorChain.proceedAsync]
 * on the interceptor chain.
 */
class ApolloServerInterceptor(
    private val serverUrl: HttpUrl,
    private val httpCallFactory: Call.Factory,
    private val cachePolicy: HttpCachePolicy.Policy?,
    val prefetch: Boolean,
    val customScalarAdapters: CustomScalarAdapters,
    val logger: ApolloLogger,
) : ApolloInterceptor {
  var httpCallRef = AtomicReference()

  @Volatile
  var disposed = false

  val composer = DefaultHttpRequestComposer(serverUrl.toString())

  override fun interceptAsync(
      request: InterceptorRequest, chain: ApolloInterceptorChain,
      dispatcher: Executor, callBack: CallBack,
  ) {
    dispatcher.execute { executeHttpCall(request, callBack) }
  }

  override fun dispose() {
    disposed = true
    val httpCall = httpCallRef.getAndSet(null)
    httpCall?.cancel()
  }

  fun executeHttpCall(request: InterceptorRequest, callBack: CallBack) {
    if (disposed) return
    callBack.onFetch(FetchSourceType.NETWORK)
    val httpCall: Call
    httpCall = try {
      if (request.useHttpGetMethodForQueries && request.operation is Query<*>) {
        httpGetCall(request.operation, request.cacheHeaders, request.requestHeaders,
            request.sendQueryDocument, request.autoPersistQueries)
      } else {
        httpPostCall(request.operation, request.cacheHeaders, request.requestHeaders,
            request.sendQueryDocument, request.autoPersistQueries)
      }
    } catch (e: IOException) {
      logger.e(e, "Failed to prepare http call for operation %s", request.operation.name())
      callBack.onFailure(ApolloNetworkException("Failed to prepare http call", e))
      return
    }
    val previousCall = httpCallRef.getAndSet(httpCall)
    previousCall?.cancel()
    if (httpCall.isCanceled || disposed) {
      httpCallRef.compareAndSet(httpCall, null)
      return
    }
    httpCall.enqueue(object : Callback {
      override fun onFailure(call: Call, e: IOException) {
        if (disposed) return
        if (httpCallRef.compareAndSet(httpCall, null)) {
          logger.e(e, "Failed to execute http call for operation %s", request.operation.name())
          callBack.onFailure(ApolloNetworkException("Failed to execute http call", e))
        }
      }

      override fun onResponse(call: Call, response: Response) {
        if (disposed) return
        if (httpCallRef.compareAndSet(httpCall, null)) {
          callBack.onResponse(InterceptorResponse(response))
          callBack.onCompleted()
        }
      }
    })
  }

  @Throws(IOException::class)
  fun httpGetCall(
      operation: Operation<*>, cacheHeaders: CacheHeaders, requestHeaders: RequestHeaders,
      writeQueryDocument: Boolean, autoPersistQueries: Boolean,
  ): Call {
    val requestBuilder = Request.Builder()
        .url(httpGetUrl(serverUrl, operation, customScalarAdapters, writeQueryDocument, autoPersistQueries))
        .get()
    decorateRequest(requestBuilder, operation, cacheHeaders, requestHeaders)
    return httpCallFactory.newCall(requestBuilder.build())
  }

  @Throws(IOException::class)
  fun httpPostCall(
      operation: Operation<*>, cacheHeaders: CacheHeaders, requestHeaders: RequestHeaders,
      writeQueryDocument: Boolean, autoPersistQueries: Boolean,
  ): Call {
    val body = composer.compose(ApolloRequest(operation)
        .withExecutionContext(customScalarAdapters)
        .withExecutionContext(
            HttpRequestComposerParams(
                method = HttpMethod.Post,
                sendApqExtensions = autoPersistQueries,
                sendDocument = writeQueryDocument,
                headers = emptyMap()
            )
        )
    ).body!!

    val requestBody = object : RequestBody() {
      override fun contentType() = MediaType.parse(body.contentType)

      override fun contentLength() = body.contentLength

      override fun writeTo(sink: BufferedSink) {
        body.writeTo(bufferedSink = sink)
      }
    }
    val requestBuilder = Request.Builder()
        .url(serverUrl)
        .post(requestBody)
    decorateRequest(requestBuilder, operation, cacheHeaders, requestHeaders)
    return httpCallFactory.newCall(requestBuilder.build())
  }

  @Throws(IOException::class)
  fun decorateRequest(
      requestBuilder: Request.Builder, operation: Operation<*>, cacheHeaders: CacheHeaders,
      requestHeaders: RequestHeaders,
  ) {
    requestBuilder
        .header(HEADER_ACCEPT_TYPE, JSON_CONTENT_TYPE)
        /**
         * Content-Type is usually taken from the RequestBody but some implementation might not use OkHttp as a CallFactory
         * and therefore use this
         */
        .header(HEADER_CONTENT_TYPE, JSON_CONTENT_TYPE)
        .header(HEADER_APOLLO_OPERATION_ID, operation.id())
        .header(HEADER_APOLLO_OPERATION_NAME, operation.name())
        .tag(operation.id())
    for (header in requestHeaders.headers()) {
      val value = requestHeaders.headerValue(header)
      requestBuilder.header(header, value)
    }
    if (cachePolicy != null) {
      val skipCacheHttpResponse = "true".equals(cacheHeaders.headerValue(
          ApolloCacheHeaders.DO_NOT_STORE), ignoreCase = true)
      val cacheKey = cacheKey(operation, customScalarAdapters)
      requestBuilder
          .header(HttpCache.CACHE_KEY_HEADER, cacheKey)
          .header(HttpCache.CACHE_FETCH_STRATEGY_HEADER, cachePolicy.fetchStrategy.name)
          .header(HttpCache.CACHE_EXPIRE_TIMEOUT_HEADER, cachePolicy.expireTimeoutMs().toString())
          .header(HttpCache.CACHE_EXPIRE_AFTER_READ_HEADER, java.lang.Boolean.toString(cachePolicy.expireAfterRead))
          .header(HttpCache.CACHE_PREFETCH_HEADER, java.lang.Boolean.toString(prefetch))
          .header(HttpCache.CACHE_DO_NOT_STORE, java.lang.Boolean.toString(skipCacheHttpResponse))
    }
  }

  companion object {
    const val HEADER_ACCEPT_TYPE = "Accept"
    const val HEADER_CONTENT_TYPE = "Content-Type"
    const val HEADER_APOLLO_OPERATION_ID = "X-APOLLO-OPERATION-ID"
    const val HEADER_APOLLO_OPERATION_NAME = "X-APOLLO-OPERATION-NAME"
    const val JSON_CONTENT_TYPE = "application/json"

    @Throws(IOException::class)
    fun cacheKey(operation: Operation<*>, customScalarAdapters: CustomScalarAdapters): String {
      return DefaultHttpRequestComposer.buildParamsMap(
          operation = operation,
          autoPersistQueries = true,
          sendDocument = true,
          customScalarAdapters = customScalarAdapters
      ).md5().hex()
    }

    @Throws(IOException::class)
    fun httpGetUrl(
        serverUrl: HttpUrl, operation: Operation<*>,
        customScalarAdapters: CustomScalarAdapters?, writeQueryDocument: Boolean,
        autoPersistQueries: Boolean,
    ): HttpUrl {
      val urlBuilder = serverUrl.newBuilder()
      if (!autoPersistQueries || writeQueryDocument) {
        urlBuilder.addQueryParameter("query", operation.document())
      }
      addVariablesUrlQueryParameter(urlBuilder, operation, customScalarAdapters)

      urlBuilder.addQueryParameter("operationName", operation.name())
      if (autoPersistQueries) {
        addExtensionsUrlQueryParameter(urlBuilder, operation)
      }
      return urlBuilder.build()
    }

    @Throws(IOException::class)
    fun addVariablesUrlQueryParameter(
        urlBuilder: HttpUrl.Builder,
        operation: Operation<*>,
        customScalarAdapters: CustomScalarAdapters?,
    ) {
      val buffer = Buffer()
      val jsonWriter = BufferedSinkJsonWriter(buffer)
      jsonWriter.beginObject()
      operation.serializeVariables(jsonWriter, customScalarAdapters!!)
      jsonWriter.endObject()
      jsonWriter.close()
      urlBuilder.addQueryParameter("variables", buffer.readUtf8())
    }

    @Throws(IOException::class)
    fun addExtensionsUrlQueryParameter(urlBuilder: HttpUrl.Builder, operation: Operation<*>) {
      val buffer = Buffer()
      val jsonWriter = BufferedSinkJsonWriter(buffer)
      jsonWriter.beginObject()
      jsonWriter.name("persistedQuery")
          .beginObject()
          .name("version").value(1)
          .name("sha256Hash").value(operation.id())
          .endObject()
      jsonWriter.endObject()
      jsonWriter.close()
      urlBuilder.addQueryParameter("extensions", buffer.readUtf8())
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy