diff --git a/libraries/apollo-api/api/apollo-api.api b/libraries/apollo-api/api/apollo-api.api index decbf1509df..1073f48fdfc 100644 --- a/libraries/apollo-api/api/apollo-api.api +++ b/libraries/apollo-api/api/apollo-api.api @@ -979,8 +979,9 @@ public final class com/apollographql/apollo3/api/http/HttpMethod : java/lang/Enu } public final class com/apollographql/apollo3/api/http/HttpRequest { - public synthetic fun (Lcom/apollographql/apollo3/api/http/HttpMethod;Ljava/lang/String;Ljava/util/List;Lcom/apollographql/apollo3/api/http/HttpBody;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lcom/apollographql/apollo3/api/http/HttpMethod;Ljava/lang/String;Ljava/util/List;Lcom/apollographql/apollo3/api/http/HttpBody;Lcom/apollographql/apollo3/api/ExecutionContext;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getBody ()Lcom/apollographql/apollo3/api/http/HttpBody; + public final fun getExecutionContext ()Lcom/apollographql/apollo3/api/ExecutionContext; public final fun getHeaders ()Ljava/util/List; public final fun getMethod ()Lcom/apollographql/apollo3/api/http/HttpMethod; public final fun getUrl ()Ljava/lang/String; @@ -992,6 +993,7 @@ public final class com/apollographql/apollo3/api/http/HttpRequest { public final class com/apollographql/apollo3/api/http/HttpRequest$Builder { public fun (Lcom/apollographql/apollo3/api/http/HttpMethod;Ljava/lang/String;)V + public final fun addExecutionContext (Lcom/apollographql/apollo3/api/ExecutionContext;)Lcom/apollographql/apollo3/api/http/HttpRequest$Builder; public final fun addHeader (Ljava/lang/String;Ljava/lang/String;)Lcom/apollographql/apollo3/api/http/HttpRequest$Builder; public final fun addHeaders (Ljava/util/List;)Lcom/apollographql/apollo3/api/http/HttpRequest$Builder; public final fun body (Lcom/apollographql/apollo3/api/http/HttpBody;)Lcom/apollographql/apollo3/api/http/HttpRequest$Builder; diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/http/DefaultHttpRequestComposer.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/http/DefaultHttpRequestComposer.kt index 1610b057642..eac857487ef 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/http/DefaultHttpRequestComposer.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/http/DefaultHttpRequestComposer.kt @@ -53,13 +53,12 @@ class DefaultHttpRequestComposer( val sendApqExtensions = apolloRequest.sendApqExtensions ?: false val sendDocument = apolloRequest.sendDocument ?: true - return when (apolloRequest.httpMethod ?: HttpMethod.Post) { + val httpRequestBuilder = when (apolloRequest.httpMethod ?: HttpMethod.Post) { HttpMethod.Get -> { HttpRequest.Builder( method = HttpMethod.Get, url = buildGetUrl(serverUrl, operation, customScalarAdapters, sendApqExtensions, sendDocument), - ).addHeaders(requestHeaders) - .build() + ) } HttpMethod.Post -> { @@ -67,11 +66,14 @@ class DefaultHttpRequestComposer( HttpRequest.Builder( method = HttpMethod.Post, url = serverUrl, - ).addHeaders(requestHeaders) - .body(buildPostBody(operation, customScalarAdapters, sendApqExtensions, query)) - .build() + ).body(buildPostBody(operation, customScalarAdapters, sendApqExtensions, query)) } } + + return httpRequestBuilder + .addHeaders(requestHeaders) + .addExecutionContext(apolloRequest.executionContext) + .build() } companion object { @@ -149,7 +151,7 @@ class DefaultHttpRequestComposer( /** * This mostly duplicates [composePostParams] but encode variables and extensions as strings - * and not json elements. I tried factoring in that code but it ended up being more clunky that + * and not json elements. I tried factoring in that code, but it ended up being more clunky that * duplicating it */ private fun composeGetParams( diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/http/Http.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/http/Http.kt index 1077580ce09..bf8532c49d3 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/http/Http.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/http/Http.kt @@ -3,6 +3,7 @@ package com.apollographql.apollo3.api.http import com.apollographql.apollo3.annotations.ApolloDeprecatedSince import com.apollographql.apollo3.annotations.ApolloDeprecatedSince.Version.v3_4_1 import com.apollographql.apollo3.annotations.ApolloExperimental +import com.apollographql.apollo3.api.ExecutionContext import okio.Buffer import okio.BufferedSink import okio.BufferedSource @@ -35,7 +36,7 @@ data class HttpHeader(val name: String, val value: String) /** * Get the value for header [name] or null if this header doesn't exist or is defined multiple times * - * The header name matching is case insensitive + * The header name matching is case-insensitive */ @ApolloExperimental fun List.get(name: String): String? { @@ -51,6 +52,7 @@ private constructor( val url: String, val headers: List, val body: HttpBody?, + val executionContext: ExecutionContext ) { @JvmOverloads @@ -67,12 +69,13 @@ private constructor( /** * The URL to send the request to. - * Must be conform to [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986#section-2). + * Must conform to [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986#section-2). */ private val url: String, ) { private var body: HttpBody? = null private val headers = mutableListOf() + private var executionContext = ExecutionContext.Empty fun body(body: HttpBody) = apply { this.body = body @@ -86,17 +89,21 @@ private constructor( this.headers.addAll(headers) } + fun addExecutionContext(executionContext: ExecutionContext) = apply { + this.executionContext += executionContext + } + fun headers(headers: List) = apply { this.headers.clear() this.headers.addAll(headers) } - @Suppress("DEPRECATION") fun build() = HttpRequest( method = method, url = url, headers = headers, body = body, + executionContext = executionContext ) } } @@ -106,7 +113,7 @@ private constructor( * * Specifying both [bodySource] and [bodyString] is invalid * - * The [body] of a [HttpResponse] must always be closed if non null + * The [body] of a [HttpResponse] must always be closed if non-null */ class HttpResponse private constructor( @@ -177,7 +184,6 @@ private constructor( } fun build(): HttpResponse { - @Suppress("DEPRECATION") return HttpResponse( statusCode = statusCode, headers = headers, diff --git a/libraries/apollo-runtime/api/apollo-runtime.api b/libraries/apollo-runtime/api/apollo-runtime.api index 9f56b3dc13c..e13fd26a118 100644 --- a/libraries/apollo-runtime/api/apollo-runtime.api +++ b/libraries/apollo-runtime/api/apollo-runtime.api @@ -203,6 +203,7 @@ public final class com/apollographql/apollo3/network/http/BatchingHttpIntercepto } public final class com/apollographql/apollo3/network/http/DefaultHttpEngine : com/apollographql/apollo3/network/http/HttpEngine { + public static final field Companion Lcom/apollographql/apollo3/network/http/DefaultHttpEngine$Companion; public fun (J)V public synthetic fun (JILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun (JJ)V @@ -212,6 +213,12 @@ public final class com/apollographql/apollo3/network/http/DefaultHttpEngine : co public fun execute (Lcom/apollographql/apollo3/api/http/HttpRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public final class com/apollographql/apollo3/network/http/DefaultHttpEngine$Companion { + public final fun execute (Lokhttp3/Call$Factory;Lokhttp3/Request;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; + public final fun toApolloHttpResponse (Lokhttp3/Response;)Lcom/apollographql/apollo3/api/http/HttpResponse; + public final fun toOkHttpRequest (Lcom/apollographql/apollo3/api/http/HttpRequest;)Lokhttp3/Request; +} + public final class com/apollographql/apollo3/network/http/HeadersInterceptor : com/apollographql/apollo3/network/http/HttpInterceptor { public fun (Ljava/util/List;)V public fun intercept (Lcom/apollographql/apollo3/api/http/HttpRequest;Lcom/apollographql/apollo3/network/http/HttpInterceptorChain;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -219,6 +226,7 @@ public final class com/apollographql/apollo3/network/http/HeadersInterceptor : c public final class com/apollographql/apollo3/network/http/HttpCall { public fun (Lcom/apollographql/apollo3/network/http/HttpEngine;Lcom/apollographql/apollo3/api/http/HttpMethod;Ljava/lang/String;)V + public final fun addExecutionContext (Lcom/apollographql/apollo3/api/ExecutionContext;)Lcom/apollographql/apollo3/network/http/HttpCall; public final fun addHeader (Ljava/lang/String;Ljava/lang/String;)Lcom/apollographql/apollo3/network/http/HttpCall; public final fun addHeaders (Ljava/util/List;)Lcom/apollographql/apollo3/network/http/HttpCall; public final fun body (Lcom/apollographql/apollo3/api/http/HttpBody;)Lcom/apollographql/apollo3/network/http/HttpCall; diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/http/HttpEngine.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/http/HttpEngine.kt index 472f9651e4c..e3315af9758 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/http/HttpEngine.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/http/HttpEngine.kt @@ -1,5 +1,6 @@ package com.apollographql.apollo3.network.http +import com.apollographql.apollo3.api.ExecutionContext import com.apollographql.apollo3.api.http.HttpBody import com.apollographql.apollo3.api.http.HttpHeader import com.apollographql.apollo3.api.http.HttpMethod @@ -30,7 +31,7 @@ interface HttpEngine { } /** - * @param timeoutMillis: The timeout interval to use when connecting or waiting for additional data. + * @param timeoutMillis The timeout interval to use when connecting or waiting for additional data. * * - on iOS (NSURLRequest), it is used to set `NSMutableURLRequest.setTimeoutInterval` * - on Android (OkHttp), it is used to set both `OkHttpClient.connectTimeout` and `OkHttpClient.readTimeout` @@ -60,6 +61,9 @@ class HttpCall(private val engine: HttpEngine, method: HttpMethod, url: String) requestBuilder.addHeaders(headers) } + fun addExecutionContext(executionContext: ExecutionContext) = apply { + requestBuilder.addExecutionContext(executionContext) + } fun headers(headers: List) = apply { requestBuilder.headers(headers) } diff --git a/libraries/apollo-runtime/src/jvmMain/kotlin/com/apollographql/apollo3/network/http/OkHttpEngine.kt b/libraries/apollo-runtime/src/jvmMain/kotlin/com/apollographql/apollo3/network/http/OkHttpEngine.kt index a54e94a4d29..27af19d310c 100644 --- a/libraries/apollo-runtime/src/jvmMain/kotlin/com/apollographql/apollo3/network/http/OkHttpEngine.kt +++ b/libraries/apollo-runtime/src/jvmMain/kotlin/com/apollographql/apollo3/network/http/OkHttpEngine.kt @@ -13,6 +13,7 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody +import okhttp3.Response import okio.BufferedSink import okio.IOException import java.util.concurrent.TimeUnit @@ -35,75 +36,84 @@ actual class DefaultHttpEngine constructor( .build() ) - actual override suspend fun execute(request: HttpRequest): HttpResponse = suspendCancellableCoroutine { continuation -> - val httpRequest = Request.Builder() - .url(request.url) - .headers(request.headers.toOkHttpHeaders()) - .apply { - if (request.method == HttpMethod.Get) { - get() - } else { - val body = request.body - check(body != null) { - "HTTP POST requires a request body" - } - post(object : RequestBody() { - override fun contentType() = body.contentType.toMediaType() - - override fun contentLength() = body.contentLength + actual override suspend fun execute(request: HttpRequest): HttpResponse { + return httpCallFactory.execute(request.toOkHttpRequest()).toApolloHttpResponse() + } - // This prevents OkHttp from reading the body several times (e.g. when using its logging interceptor) - // which could consume files when using Uploads - override fun isOneShot() = body is UploadsHttpBody + actual override fun dispose() { + } - override fun writeTo(sink: BufferedSink) { - body.writeTo(sink) + companion object { + fun HttpRequest.toOkHttpRequest(): Request { + return Request.Builder() + .url(url) + .headers(headers.toOkHttpHeaders()) + .apply { + if (method == HttpMethod.Get) { + get() + } else { + val body = body + check(body != null) { + "HTTP POST requires a request body" } - }) - } - } - .build() + post(object : RequestBody() { + override fun contentType() = body.contentType.toMediaType() + + override fun contentLength() = body.contentLength - val call = httpCallFactory.newCall(httpRequest) - continuation.invokeOnCancellation { - call.cancel() + // This prevents OkHttp from reading the body several times (e.g. when using its logging interceptor) + // which could consume files when using Uploads + override fun isOneShot() = body is UploadsHttpBody + + override fun writeTo(sink: BufferedSink) { + body.writeTo(sink) + } + }) + } + } + .build() } - var exception: IOException? = null - val response = try { - call.execute() - } catch (e: IOException) { - exception = e - null + suspend fun Call.Factory.execute(request: Request): Response = suspendCancellableCoroutine { continuation -> + val call = newCall(request) + continuation.invokeOnCancellation { + call.cancel() + } + + var exception: IOException? = null + val response = try { + call.execute() + } catch (e: IOException) { + exception = e + null + } + + if (exception != null) { + continuation.resumeWithException( + ApolloNetworkException( + message = "Failed to execute GraphQL http network request", + platformCause = exception + ) + ) + return@suspendCancellableCoroutine + } else { + continuation.resume(response!!) + } } - if (exception != null) { - continuation.resumeWithException( - ApolloNetworkException( - message = "Failed to execute GraphQL http network request", - platformCause = exception + fun Response.toApolloHttpResponse(): HttpResponse { + return HttpResponse.Builder(statusCode = code) + .body(body!!.source()) + .addHeaders( + headers.let { headers -> + 0.until(headers.size).map { index -> + HttpHeader(headers.name(index), headers.value(index)) + } + } ) - ) - return@suspendCancellableCoroutine - } else { - val result = Result.success( - HttpResponse.Builder(statusCode = response!!.code) - .body(response.body!!.source()) - .addHeaders( - response.headers.let { headers -> - 0.until(headers.size).map { index -> - HttpHeader(headers.name(index), headers.value(index)) - } - } - ) - .build() - ) - continuation.resume(result.getOrThrow()) + .build() } } - - actual override fun dispose() { - } } diff --git a/tests/integration-tests/src/jvmTest/kotlin/test/ExecutionContextTest.kt b/tests/integration-tests/src/jvmTest/kotlin/test/ExecutionContextTest.kt new file mode 100644 index 00000000000..41f0b00ded5 --- /dev/null +++ b/tests/integration-tests/src/jvmTest/kotlin/test/ExecutionContextTest.kt @@ -0,0 +1,77 @@ +package test + +import com.apollographql.apollo3.ApolloClient +import com.apollographql.apollo3.api.ExecutionContext +import com.apollographql.apollo3.api.http.HttpRequest +import com.apollographql.apollo3.api.http.HttpResponse +import com.apollographql.apollo3.integration.normalizer.HeroNameQuery +import com.apollographql.apollo3.mockserver.MockServer +import com.apollographql.apollo3.mockserver.enqueueString +import com.apollographql.apollo3.network.http.DefaultHttpEngine.Companion.execute +import com.apollographql.apollo3.network.http.DefaultHttpEngine.Companion.toApolloHttpResponse +import com.apollographql.apollo3.network.http.DefaultHttpEngine.Companion.toOkHttpRequest +import com.apollographql.apollo3.network.http.HttpEngine +import com.apollographql.apollo3.testing.internal.runTest +import okhttp3.OkHttpClient +import org.junit.Test +import kotlin.test.assertEquals + +internal class MyHttpEngine: HttpEngine { + val values = mutableListOf() + + private val okHttpClient = OkHttpClient() + + override suspend fun execute(request: HttpRequest): HttpResponse { + val myExecutionContext = request.executionContext[MyExecutionContext]?.also { + values.add(it.value) + } + val taggedRequest = request + .toOkHttpRequest() + .newBuilder() + .tag(MyExecutionContext::class.java, myExecutionContext) + .build() + return okHttpClient.execute(taggedRequest).toApolloHttpResponse() + } + + override fun dispose() { + + } +} + +class ExecutionContextTest { + @Test + fun executionContextIsAvailableInHttpInterceptor() = runTest { + + MockServer().use { mockServer -> + val myHttpEngine = MyHttpEngine() + ApolloClient.Builder() + .serverUrl(mockServer.url()) + .httpEngine(myHttpEngine) + .build().use { apolloClient -> + + // we don't need a response + mockServer.enqueueString(statusCode = 404) + apolloClient.query(HeroNameQuery()) + .addExecutionContext(MyExecutionContext("value0")) + .execute() + + mockServer.enqueueString(statusCode = 404) + apolloClient.query(HeroNameQuery()) + .addExecutionContext(MyExecutionContext("value1")) + .execute() + + + assertEquals(2, myHttpEngine.values.size) + assertEquals("value0", myHttpEngine.values[0]) + assertEquals("value1", myHttpEngine.values[1]) + } + } + } +} + +class MyExecutionContext(val value: String): ExecutionContext.Element { + companion object Key: ExecutionContext.Key + + override val key: ExecutionContext.Key + get() = Key +} \ No newline at end of file