From f39a19344ca99634f2db38db0fdbfc90c696bd04 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sun, 11 May 2025 10:47:47 +0100 Subject: [PATCH 1/4] Call decorator --- android-test/build.gradle.kts | 1 + .../java/okhttp/android/test/AlwaysHttps.kt | 75 +++++++ .../android/test/AndroidCallDecoratorTest.kt | 100 +++++++++ .../java/okhttp/android/test/OffMainThread.kt | 41 ++++ okhttp/api/android/okhttp.api | 12 ++ okhttp/api/jvm/okhttp.api | 12 ++ .../commonJvmAndroid/kotlin/okhttp3/Call.kt | 29 +++ .../kotlin/okhttp3/OkHttpClient.kt | 38 +++- .../kotlin/okhttp3/CallDecoratorTest.kt | 204 ++++++++++++++++++ 9 files changed, 511 insertions(+), 1 deletion(-) create mode 100644 android-test/src/androidTest/java/okhttp/android/test/AlwaysHttps.kt create mode 100644 android-test/src/androidTest/java/okhttp/android/test/AndroidCallDecoratorTest.kt create mode 100644 android-test/src/androidTest/java/okhttp/android/test/OffMainThread.kt create mode 100644 okhttp/src/jvmTest/kotlin/okhttp3/CallDecoratorTest.kt diff --git a/android-test/build.gradle.kts b/android-test/build.gradle.kts index 8b97915e931b..8d45a2d4087b 100644 --- a/android-test/build.gradle.kts +++ b/android-test/build.gradle.kts @@ -65,6 +65,7 @@ dependencies { "friendsImplementation"(projects.okhttpDnsoverhttps) testImplementation(projects.okhttp) + testImplementation(projects.okhttpCoroutines) testImplementation(libs.junit) testImplementation(libs.junit.ktx) testImplementation(libs.assertk) diff --git a/android-test/src/androidTest/java/okhttp/android/test/AlwaysHttps.kt b/android-test/src/androidTest/java/okhttp/android/test/AlwaysHttps.kt new file mode 100644 index 000000000000..063f0c1c597f --- /dev/null +++ b/android-test/src/androidTest/java/okhttp/android/test/AlwaysHttps.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2025 Block, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okhttp.android.test + +import android.os.Build +import android.security.NetworkSecurityPolicy +import okhttp3.Call +import okhttp3.Request + +class AlwaysHttps( + policy: Policy, +) : Call.Decorator { + val hostPolicy: HostPolicy = policy.hostPolicy + + override fun newCall(chain: Call.Chain): Call { + val request = chain.request + + val updatedRequest = + if (request.url.scheme == "http" && !hostPolicy.isCleartextTrafficPermitted(request)) { + request + .newBuilder() + .url( + request.url + .newBuilder() + .scheme("https") + .build(), + ).build() + } else { + request + } + + return chain.proceed(updatedRequest) + } + + fun interface HostPolicy { + fun isCleartextTrafficPermitted(request: Request): Boolean + } + + enum class Policy { + Always { + override val hostPolicy: HostPolicy + get() = HostPolicy { false } + }, + Manifest { + override val hostPolicy: HostPolicy + get() = + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) { + val networkSecurityPolicy = NetworkSecurityPolicy.getInstance() + + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) { + HostPolicy { networkSecurityPolicy.isCleartextTrafficPermitted(it.url.host) } + } else { + HostPolicy { networkSecurityPolicy.isCleartextTrafficPermitted } + } + } else { + HostPolicy { true } + } + }, ; + + abstract val hostPolicy: HostPolicy + } +} diff --git a/android-test/src/androidTest/java/okhttp/android/test/AndroidCallDecoratorTest.kt b/android-test/src/androidTest/java/okhttp/android/test/AndroidCallDecoratorTest.kt new file mode 100644 index 000000000000..9f062fc01354 --- /dev/null +++ b/android-test/src/androidTest/java/okhttp/android/test/AndroidCallDecoratorTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2025 Block, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okhttp.android.test + +import java.util.logging.Logger +import mockwebserver3.MockResponse +import mockwebserver3.MockWebServer +import mockwebserver3.junit5.StartStop +import okhttp.android.test.AlwaysHttps.Policy +import okhttp3.OkHttpClient +import okhttp3.OkHttpClientTestRule +import okhttp3.Request +import okhttp3.tls.internal.TlsUtil.localhost +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +@Tag("Slow") +class AndroidCallDecoratorTest { + @Suppress("RedundantVisibilityModifier") + @JvmField + @RegisterExtension + public val clientTestRule = + OkHttpClientTestRule().apply { + logger = Logger.getLogger(AndroidCallDecoratorTest::class.java.name) + } + + private var client: OkHttpClient = + clientTestRule + .newClientBuilder() + .addCallDecorator(AlwaysHttps(Policy.Always)) + .addCallDecorator(OffMainThread) + .build() + + @StartStop + private val server = MockWebServer() + + private val handshakeCertificates = localhost() + + @Test + fun testSecureRequest() { + enableTls() + + server.enqueue(MockResponse()) + + val request = Request.Builder().url(server.url("/")).build() + + client.newCall(request).execute().use { + assertEquals(200, it.code) + } + } + + @Test + fun testInsecureRequestChangedToSecure() { + enableTls() + + server.enqueue(MockResponse()) + + val request = + Request + .Builder() + .url( + server + .url("/") + .newBuilder() + .scheme("http") + .build(), + ).build() + + client.newCall(request).execute().use { + assertEquals(200, it.code) + assertEquals("https", it.request.url.scheme) + } + } + + private fun enableTls() { + client = + client + .newBuilder() + .sslSocketFactory( + handshakeCertificates.sslSocketFactory(), + handshakeCertificates.trustManager, + ).build() + server.useHttps(handshakeCertificates.sslSocketFactory()) + } +} diff --git a/android-test/src/androidTest/java/okhttp/android/test/OffMainThread.kt b/android-test/src/androidTest/java/okhttp/android/test/OffMainThread.kt new file mode 100644 index 000000000000..4fff9c5bb48b --- /dev/null +++ b/android-test/src/androidTest/java/okhttp/android/test/OffMainThread.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2025 Block, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okhttp.android.test + +import android.os.Looper +import okhttp3.Call +import okhttp3.Response + +/** + * Sample of a Decorator that will fail any call on the Android Main thread. + */ +object OffMainThread : Call.Decorator { + override fun newCall(chain: Call.Chain): Call = StrictModeCall(chain.proceed(chain.request)) + + private class StrictModeCall( + private val delegate: Call, + ) : Call by delegate { + override fun execute(): Response { + if (Looper.getMainLooper() === Looper.myLooper()) { + throw IllegalStateException("Network on main thread") + } + + return delegate.execute() + } + + override fun clone(): Call = StrictModeCall(delegate.clone()) + } +} diff --git a/okhttp/api/android/okhttp.api b/okhttp/api/android/okhttp.api index 32d88d388d8e..0e73b572894f 100644 --- a/okhttp/api/android/okhttp.api +++ b/okhttp/api/android/okhttp.api @@ -129,6 +129,16 @@ public abstract interface class okhttp3/Call : java/lang/Cloneable { public abstract fun timeout ()Lokio/Timeout; } +public abstract interface class okhttp3/Call$Chain { + public abstract fun getClient ()Lokhttp3/OkHttpClient; + public abstract fun getRequest ()Lokhttp3/Request; + public abstract fun proceed (Lokhttp3/Request;)Lokhttp3/Call; +} + +public abstract interface class okhttp3/Call$Decorator { + public abstract fun newCall (Lokhttp3/Call$Chain;)Lokhttp3/Call; +} + public abstract interface class okhttp3/Call$Factory { public abstract fun newCall (Lokhttp3/Request;)Lokhttp3/Call; } @@ -902,6 +912,7 @@ public class okhttp3/OkHttpClient : okhttp3/Call$Factory, okhttp3/WebSocket$Fact public final fun fastFallback ()Z public final fun followRedirects ()Z public final fun followSslRedirects ()Z + public final fun getCallDecorators ()Ljava/util/List; public final fun hostnameVerifier ()Ljavax/net/ssl/HostnameVerifier; public final fun interceptors ()Ljava/util/List; public final fun minWebSocketMessageToCompress ()J @@ -927,6 +938,7 @@ public final class okhttp3/OkHttpClient$Builder { public final fun -addInterceptor (Lkotlin/jvm/functions/Function1;)Lokhttp3/OkHttpClient$Builder; public final fun -addNetworkInterceptor (Lkotlin/jvm/functions/Function1;)Lokhttp3/OkHttpClient$Builder; public fun ()V + public final fun addCallDecorator (Lokhttp3/Call$Decorator;)Lokhttp3/OkHttpClient$Builder; public final fun addInterceptor (Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient$Builder; public final fun addNetworkInterceptor (Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient$Builder; public final fun authenticator (Lokhttp3/Authenticator;)Lokhttp3/OkHttpClient$Builder; diff --git a/okhttp/api/jvm/okhttp.api b/okhttp/api/jvm/okhttp.api index 0133737243fc..040ea91cc844 100644 --- a/okhttp/api/jvm/okhttp.api +++ b/okhttp/api/jvm/okhttp.api @@ -129,6 +129,16 @@ public abstract interface class okhttp3/Call : java/lang/Cloneable { public abstract fun timeout ()Lokio/Timeout; } +public abstract interface class okhttp3/Call$Chain { + public abstract fun getClient ()Lokhttp3/OkHttpClient; + public abstract fun getRequest ()Lokhttp3/Request; + public abstract fun proceed (Lokhttp3/Request;)Lokhttp3/Call; +} + +public abstract interface class okhttp3/Call$Decorator { + public abstract fun newCall (Lokhttp3/Call$Chain;)Lokhttp3/Call; +} + public abstract interface class okhttp3/Call$Factory { public abstract fun newCall (Lokhttp3/Request;)Lokhttp3/Call; } @@ -901,6 +911,7 @@ public class okhttp3/OkHttpClient : okhttp3/Call$Factory, okhttp3/WebSocket$Fact public final fun fastFallback ()Z public final fun followRedirects ()Z public final fun followSslRedirects ()Z + public final fun getCallDecorators ()Ljava/util/List; public final fun hostnameVerifier ()Ljavax/net/ssl/HostnameVerifier; public final fun interceptors ()Ljava/util/List; public final fun minWebSocketMessageToCompress ()J @@ -926,6 +937,7 @@ public final class okhttp3/OkHttpClient$Builder { public final fun -addInterceptor (Lkotlin/jvm/functions/Function1;)Lokhttp3/OkHttpClient$Builder; public final fun -addNetworkInterceptor (Lkotlin/jvm/functions/Function1;)Lokhttp3/OkHttpClient$Builder; public fun ()V + public final fun addCallDecorator (Lokhttp3/Call$Decorator;)Lokhttp3/OkHttpClient$Builder; public final fun addInterceptor (Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient$Builder; public final fun addNetworkInterceptor (Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient$Builder; public final fun authenticator (Lokhttp3/Authenticator;)Lokhttp3/OkHttpClient$Builder; diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Call.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Call.kt index fdd3d3da294e..371bd4c715e2 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Call.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Call.kt @@ -96,4 +96,33 @@ interface Call : Cloneable { fun interface Factory { fun newCall(request: Request): Call } + + /** + * The equivalent of an Interceptor for [Call.Factory], but supported directly within [OkHttpClient] newCall. + * + * An [Interceptor] forms a chain as part of execution of a Call. Instead, Call.Decorator intercepts + * [Call.Factory.newCall] with similar flexibility to Application [OkHttpClient.interceptors]. + * + * That is, it may do any of + * - Modify the request such as adding Tracing Context + * - Wrap the [Call] returned + * - Return some [Call] implementation that will immediately fail avoiding network calls based on network or + * authentication state. + * - Redirect the [Call], such as using an alternative [Call.Factory]. + * - Defer execution, something not safe in an Interceptor. + * + * It should not throw an exception, instead it should return a Call that will fail on [Call.execute]. + * + * A Decorator that changes the OkHttpClient should typically retain later decorators in the new client. + */ + fun interface Decorator { + fun newCall(chain: Chain): Call + } + + interface Chain { + val client: OkHttpClient + val request: Request + + fun proceed(request: Request): Call + } } diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/OkHttpClient.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/OkHttpClient.kt index de3e75d5e701..39739f422061 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/OkHttpClient.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/OkHttpClient.kt @@ -145,6 +145,14 @@ open class OkHttpClient internal constructor( val interceptors: List = builder.interceptors.toImmutableList() + /** + * Returns an immutable list of Call decorators that have a chance to return a different, likely + * decorating, implementation of Call. This allows functionality such as fail fast without normal Call + * execution based on network conditions, or setting Tracing context on the calling thread. + */ + val callDecorators: List = + builder.callDecorators.toImmutableList() + /** * Returns an immutable list of interceptors that observe a single network request and response. * These interceptors must call [Interceptor.Chain.proceed] exactly once: it is an error for @@ -265,6 +273,27 @@ open class OkHttpClient internal constructor( internal val routeDatabase: RouteDatabase = builder.routeDatabase ?: RouteDatabase() internal val taskRunner: TaskRunner = builder.taskRunner ?: TaskRunner.INSTANCE + private val decoratedCallFactory = + callDecorators.foldRight( + Call.Factory { request -> + RealCall(client = this, originalRequest = request, forWebSocket = false) + }, + ) { callDecorator, next -> + Call.Factory { request -> + callDecorator.newCall( + object : Call.Chain { + override val client: OkHttpClient + get() = this@OkHttpClient + + override val request: Request + get() = request + + override fun proceed(request: Request): Call = next.newCall(request) + }, + ) + } + } + @get:JvmName("connectionPool") val connectionPool: ConnectionPool = builder.connectionPool ?: ConnectionPool( @@ -359,7 +388,7 @@ open class OkHttpClient internal constructor( } /** Prepares the [request] to be executed at some point in the future. */ - override fun newCall(request: Request): Call = RealCall(this, request, forWebSocket = false) + override fun newCall(request: Request): Call = decoratedCallFactory.newCall(request) /** Uses [request] to connect a new web socket. */ override fun newWebSocket( @@ -596,6 +625,7 @@ open class OkHttpClient internal constructor( internal var dispatcher: Dispatcher = Dispatcher() internal var connectionPool: ConnectionPool? = null internal val interceptors: MutableList = mutableListOf() + internal val callDecorators: MutableList = mutableListOf() internal val networkInterceptors: MutableList = mutableListOf() internal var eventListenerFactory: EventListener.Factory = EventListener.NONE.asFactory() internal var retryOnConnectionFailure = true @@ -631,6 +661,7 @@ open class OkHttpClient internal constructor( this.dispatcher = okHttpClient.dispatcher this.connectionPool = okHttpClient.connectionPool this.interceptors += okHttpClient.interceptors + this.callDecorators += okHttpClient.callDecorators this.networkInterceptors += okHttpClient.networkInterceptors this.eventListenerFactory = okHttpClient.eventListenerFactory this.retryOnConnectionFailure = okHttpClient.retryOnConnectionFailure @@ -735,6 +766,11 @@ open class OkHttpClient internal constructor( this.eventListenerFactory = eventListenerFactory } + fun addCallDecorator(decorator: Call.Decorator) = + apply { + callDecorators += decorator + } + /** * Configure this client to retry or not when a connectivity problem is encountered. By default, * this client silently recovers from the following problems: diff --git a/okhttp/src/jvmTest/kotlin/okhttp3/CallDecoratorTest.kt b/okhttp/src/jvmTest/kotlin/okhttp3/CallDecoratorTest.kt new file mode 100644 index 000000000000..9f4c929f8103 --- /dev/null +++ b/okhttp/src/jvmTest/kotlin/okhttp3/CallDecoratorTest.kt @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2025 Block, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okhttp3 + +import java.io.IOException +import java.util.logging.Logger +import mockwebserver3.MockResponse +import mockwebserver3.MockWebServer +import mockwebserver3.junit5.StartStop +import okhttp3.internal.connection.RealCall +import okhttp3.tls.internal.TlsUtil.localhost +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +@Tag("Slow") +class CallDecoratorTest { + @Suppress("RedundantVisibilityModifier") + @JvmField + @RegisterExtension + public val clientTestRule = + OkHttpClientTestRule().apply { + logger = Logger.getLogger(CallDecoratorTest::class.java.name) + } + + @StartStop + private val server = MockWebServer() + + private val handshakeCertificates = localhost() + + @Test + fun testSecureRequest() { + server.enqueue(MockResponse()) + + val request = Request.Builder().url(server.url("/")).build() + + val client: OkHttpClient = + clientTestRule + .newClientBuilder() + .enableTls() + .addCallDecorator(AlwaysHttps) + .build() + + client.newCall(request).execute().use { + assertEquals(200, it.code) + } + } + + @Test + fun testInsecureRequestChangedToSecure() { + server.enqueue(MockResponse()) + + val request = + Request + .Builder() + .url( + server + .url("/") + .newBuilder() + .scheme("http") + .build(), + ).build() + + val client: OkHttpClient = + clientTestRule + .newClientBuilder() + .enableTls() + .addCallDecorator(AlwaysHttps) + .build() + + client.newCall(request).execute().use { + assertEquals(200, it.code) + assertEquals("https", it.request.url.scheme) + } + } + + class WrappedCall( + delegate: Call, + ) : Call by delegate + + @Test + fun testWrappedCallIsObserved() { + server.enqueue(MockResponse()) + + val client: OkHttpClient = + clientTestRule + .newClientBuilder() + .addCallDecorator { chain, request -> + // First Call.Decorator will see the result of later decorators + chain.newCall(request).also { + if (it !is WrappedCall) { + throw IOException("expecting wrapped call") + } + if (it.request().tag() != "wrapped") { + throw IOException("expecting tag1") + } + } + }.addCallDecorator { chain, request -> + // Wrap here + val updatedRequest = request.newBuilder().tag("wrapped").build() + WrappedCall(chain.newCall(updatedRequest)) + }.addCallDecorator { chain, request -> + // Updated requests are seen + if (request.tag() != "wrapped") { + throw IOException("expecting tag2") + } + chain.newCall(request).also { + // But Call is RealCall + if (it !is RealCall) { + throw IOException("expecting RealCall") + } + } + }.addInterceptor { chain -> + // Updated requests are seen in interceptors + if (chain.request().tag() != "wrapped") { + throw IOException("expecting tag3") + } + chain.proceed(chain.request()) + }.addNetworkInterceptor { chain -> + // and network interceptors + if (chain.request().tag() != "wrapped") { + throw IOException("expecting tag4") + } + chain.proceed(chain.request()) + }.build() + + val originalRequest = Request.Builder().url(server.url("/")).build() + client.newCall(originalRequest).execute().use { + assertEquals(200, it.code) + } + } + + @Test + fun testCanShortCircuit() { + server.enqueue(MockResponse()) + + val request = Request.Builder().url(server.url("/")).build() + + val client: OkHttpClient = + clientTestRule + .newClientBuilder() + .build() + + val redirectingClient: OkHttpClient = + clientTestRule + .newClientBuilder() + .addCallDecorator { _, request -> + // Use the other client + client.newCall(request) + }.addInterceptor { + // Fail if we get here + throw IOException("You shall not pass") + }.build() + + redirectingClient.newCall(request).execute().use { + assertEquals(200, it.code) + } + } + + private fun OkHttpClient.Builder.enableTls(): OkHttpClient.Builder { + server.useHttps(handshakeCertificates.sslSocketFactory()) + return sslSocketFactory( + handshakeCertificates.sslSocketFactory(), + handshakeCertificates.trustManager, + ) + } +} + +private object AlwaysHttps : Call.Decorator { + override fun newCall( + chain: Call.Factory, + request: Request, + ): Call { + val updatedRequest = + if (request.url.scheme == "http") { + request + .newBuilder() + .url( + request.url + .newBuilder() + .scheme("https") + .build(), + ).build() + } else { + request + } + + return chain.newCall(updatedRequest) + } +} From db45bcb565a29b70f7cd1fadaac8c7a5942eab83 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sun, 12 Oct 2025 09:42:17 +0100 Subject: [PATCH 2/4] Switch Call.Decorator to an Interceptor --- .../java/okhttp/android/test/AlwaysHttps.kt | 8 +- .../android/test/AndroidNetworkPinningTest.kt | 125 +++++++++++++ .../java/okhttp/android/test/OffMainThread.kt | 2 +- .../okhttp3/android/AndroidNetworkPinning.kt | 55 ++++++ .../okhttp3/android/internal/AndroidDns.kt | 66 +++++++ .../commonJvmAndroid/kotlin/okhttp3/Call.kt | 29 --- .../okhttp3/ClientForkingInterceptor.kt | 40 +++++ .../kotlin/okhttp3/ExperimentalOkHttpApi.kt | 46 +++++ .../kotlin/okhttp3/OkHttpClient.kt | 38 +--- .../kotlin/okhttp3/CallDecoratorTest.kt | 165 +++++++----------- settings.gradle.kts | 4 +- 11 files changed, 406 insertions(+), 172 deletions(-) create mode 100644 android-test/src/androidTest/java/okhttp/android/test/AndroidNetworkPinningTest.kt create mode 100644 okhttp/src/androidMain/kotlin/okhttp3/android/AndroidNetworkPinning.kt create mode 100644 okhttp/src/androidMain/kotlin/okhttp3/android/internal/AndroidDns.kt create mode 100644 okhttp/src/commonJvmAndroid/kotlin/okhttp3/ClientForkingInterceptor.kt create mode 100644 okhttp/src/commonJvmAndroid/kotlin/okhttp3/ExperimentalOkHttpApi.kt diff --git a/android-test/src/androidTest/java/okhttp/android/test/AlwaysHttps.kt b/android-test/src/androidTest/java/okhttp/android/test/AlwaysHttps.kt index 063f0c1c597f..2ec97da828b3 100644 --- a/android-test/src/androidTest/java/okhttp/android/test/AlwaysHttps.kt +++ b/android-test/src/androidTest/java/okhttp/android/test/AlwaysHttps.kt @@ -18,15 +18,17 @@ package okhttp.android.test import android.os.Build import android.security.NetworkSecurityPolicy import okhttp3.Call +import okhttp3.Interceptor import okhttp3.Request +import okhttp3.Response class AlwaysHttps( policy: Policy, -) : Call.Decorator { +) : Interceptor { val hostPolicy: HostPolicy = policy.hostPolicy - override fun newCall(chain: Call.Chain): Call { - val request = chain.request + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() val updatedRequest = if (request.url.scheme == "http" && !hostPolicy.isCleartextTrafficPermitted(request)) { diff --git a/android-test/src/androidTest/java/okhttp/android/test/AndroidNetworkPinningTest.kt b/android-test/src/androidTest/java/okhttp/android/test/AndroidNetworkPinningTest.kt new file mode 100644 index 000000000000..d5c9a8b50fa8 --- /dev/null +++ b/android-test/src/androidTest/java/okhttp/android/test/AndroidNetworkPinningTest.kt @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2025 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okhttp.android.test + +import android.content.Context +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkRequest +import android.os.Build +import androidx.test.core.app.ApplicationProvider +import androidx.test.filters.SdkSuppress +import mockwebserver3.MockResponse +import mockwebserver3.MockWebServer +import mockwebserver3.junit5.StartStop +import okhttp3.OkHttpClient +import okhttp3.OkHttpClientTestRule +import okhttp3.Request +import okhttp3.android.AndroidNetworkPinning +import okhttp3.internal.connection.RealCall +import okhttp3.internal.platform.PlatformRegistry +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotEquals +import org.junit.jupiter.api.Assumptions.assumeTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.RegisterExtension + +@Tag("Slow") +@SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q) +class AndroidNetworkPinningTest { + @Suppress("RedundantVisibilityModifier") + @JvmField + @RegisterExtension + public val clientTestRule = OkHttpClientTestRule() + + val applicationContext = ApplicationProvider.getApplicationContext() + val connectivityManager = applicationContext.getSystemService(ConnectivityManager::class.java) + + val pinning = AndroidNetworkPinning() + + private var client: OkHttpClient = + clientTestRule + .newClientBuilder() + .addInterceptor(pinning) + .addInterceptor { + it.proceed( + it.request() + .newBuilder() + .header("second-decorator", "true") + .build(), + ) + }.addInterceptor { + val call = (it.call() as RealCall) + val dns = call.client.dns + it + .proceed(it.request()) + .newBuilder() + .header("used-dns", dns.javaClass.simpleName) + .build() + }.build() + + @StartStop + private val server = MockWebServer() + + @BeforeEach + fun setup() { + // Needed because of Platform.resetForTests + PlatformRegistry.applicationContext = applicationContext + + connectivityManager.registerNetworkCallback(NetworkRequest.Builder().build(), pinning.networkCallback) + } + + @Test + fun testDefaultRequest() { + server.enqueue(MockResponse(200, body = "Hello")) + + val request = Request.Builder().url(server.url("/")).build() + + val response = client.newCall(request).execute() + + response.use { + assertEquals(200, response.code) + assertNotEquals("AndroidDns", response.header("used-dns")) + assertEquals("true", response.request.header("second-decorator")) + } + } + + @Test + fun testPinnedRequest() { + server.enqueue(MockResponse(200, body = "Hello")) + + val network = connectivityManager.activeNetwork + + assumeTrue(network != null) + + val request = + Request + .Builder() + .url(server.url("/")) + .tag(network) + .build() + + val response = client.newCall(request).execute() + + response.use { + assertEquals(200, response.code) + assertEquals("AndroidDns", response.header("used-dns")) + assertEquals("true", response.request.header("second-decorator")) + } + } +} diff --git a/android-test/src/androidTest/java/okhttp/android/test/OffMainThread.kt b/android-test/src/androidTest/java/okhttp/android/test/OffMainThread.kt index 4fff9c5bb48b..88d5401b8d83 100644 --- a/android-test/src/androidTest/java/okhttp/android/test/OffMainThread.kt +++ b/android-test/src/androidTest/java/okhttp/android/test/OffMainThread.kt @@ -22,7 +22,7 @@ import okhttp3.Response /** * Sample of a Decorator that will fail any call on the Android Main thread. */ -object OffMainThread : Call.Decorator { +object OffMainThread : Interceptor { override fun newCall(chain: Call.Chain): Call = StrictModeCall(chain.proceed(chain.request)) private class StrictModeCall( diff --git a/okhttp/src/androidMain/kotlin/okhttp3/android/AndroidNetworkPinning.kt b/okhttp/src/androidMain/kotlin/okhttp3/android/AndroidNetworkPinning.kt new file mode 100644 index 000000000000..ed99a2c6b326 --- /dev/null +++ b/okhttp/src/androidMain/kotlin/okhttp3/android/AndroidNetworkPinning.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2024 Block, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okhttp3.android + +import android.net.ConnectivityManager +import android.net.Network +import android.os.Build +import androidx.annotation.RequiresApi +import okhttp3.ClientForkingInterceptor +import okhttp3.ExperimentalOkHttpApi +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.android.internal.AndroidDns +import okhttp3.internal.SuppressSignatureCheck + +/** + * Decorator that supports Network Pinning on Android via Request tags. + */ +@OptIn(ExperimentalOkHttpApi::class) +@RequiresApi(Build.VERSION_CODES.Q) +@SuppressSignatureCheck +class AndroidNetworkPinning : ClientForkingInterceptor() { + + /** ConnectivityManager.NetworkCallback that will clean up after networks are lost. */ + val networkCallback = + object : ConnectivityManager.NetworkCallback() { + override fun onLost(network: Network) { + removeClient(network) + } + } + + override fun OkHttpClient.Builder.buildForKey(key: Network): OkHttpClient { + return dns(AndroidDns(key)) + .socketFactory(key.socketFactory) + .apply { + // Keep interceptors after this one in the new client + interceptors.subList(interceptors.indexOf(this@AndroidNetworkPinning) + 1, interceptors.size).clear() + }.build() + } + + override fun clientKey(request: Request): Network? = request.tag() +} diff --git a/okhttp/src/androidMain/kotlin/okhttp3/android/internal/AndroidDns.kt b/okhttp/src/androidMain/kotlin/okhttp3/android/internal/AndroidDns.kt new file mode 100644 index 000000000000..70fc149b0781 --- /dev/null +++ b/okhttp/src/androidMain/kotlin/okhttp3/android/internal/AndroidDns.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2025 Block, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okhttp3.android.internal + +import android.net.DnsResolver +import android.net.Network +import android.os.Build +import androidx.annotation.RequiresApi +import java.net.InetAddress +import java.net.UnknownHostException +import java.util.concurrent.CompletableFuture +import okhttp3.Dns +import okhttp3.internal.SuppressSignatureCheck + +@RequiresApi(Build.VERSION_CODES.Q) +@SuppressSignatureCheck +internal class AndroidDns( + val network: Network, +) : Dns { + // API 29+ + private val dnsResolver = DnsResolver.getInstance() + + override fun lookup(hostname: String): List { + // API 24+ + val result = CompletableFuture>() + + dnsResolver.query( + network, + hostname, + DnsResolver.FLAG_EMPTY, + { it.run() }, + null, + object : DnsResolver.Callback> { + override fun onAnswer( + answer: List, + rcode: Int, + ) { + result.complete(answer) + } + + override fun onError(error: DnsResolver.DnsException) { + result.completeExceptionally( + UnknownHostException(error.message).apply { + initCause(error) + }, + ) + } + }, + ) + + return result.get() + } +} diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Call.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Call.kt index 371bd4c715e2..fdd3d3da294e 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Call.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Call.kt @@ -96,33 +96,4 @@ interface Call : Cloneable { fun interface Factory { fun newCall(request: Request): Call } - - /** - * The equivalent of an Interceptor for [Call.Factory], but supported directly within [OkHttpClient] newCall. - * - * An [Interceptor] forms a chain as part of execution of a Call. Instead, Call.Decorator intercepts - * [Call.Factory.newCall] with similar flexibility to Application [OkHttpClient.interceptors]. - * - * That is, it may do any of - * - Modify the request such as adding Tracing Context - * - Wrap the [Call] returned - * - Return some [Call] implementation that will immediately fail avoiding network calls based on network or - * authentication state. - * - Redirect the [Call], such as using an alternative [Call.Factory]. - * - Defer execution, something not safe in an Interceptor. - * - * It should not throw an exception, instead it should return a Call that will fail on [Call.execute]. - * - * A Decorator that changes the OkHttpClient should typically retain later decorators in the new client. - */ - fun interface Decorator { - fun newCall(chain: Chain): Call - } - - interface Chain { - val client: OkHttpClient - val request: Request - - fun proceed(request: Request): Call - } } diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ClientForkingInterceptor.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ClientForkingInterceptor.kt new file mode 100644 index 000000000000..5e845e0d3677 --- /dev/null +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ClientForkingInterceptor.kt @@ -0,0 +1,40 @@ +package okhttp3 + +import java.io.IOException +import java.util.concurrent.ConcurrentHashMap +import okhttp3.internal.connection.RealCall + +@ExperimentalOkHttpApi +// Inspiration from https://publicobject.com/2017/04/02/a-clever-flawed-okhttp-interceptor-hack/ +// But behind an experimental but official API +abstract class ClientForkingInterceptor : Interceptor { + // TODO consider caching by client and cleaning up + private val forkedClients = ConcurrentHashMap() + + // TODO consider whether we need to address lifecycle for cleanup of clients +// override fun onNewClientInstance(client: OkHttpClient): Interceptor { +// return this +// } + + fun removeClient(key: K) { + forkedClients.remove(key) + } + + override fun intercept(chain: Interceptor.Chain): Response { + val client = + (chain.call() as? RealCall)?.client ?: throw IOException("unable to access OkHttpClient") + + val key = clientKey(chain.request()) + + if (key == null) { + return chain.proceed(chain.request()) + } else { + val override = forkedClients.getOrPut(key) { client.newBuilder().buildForKey(key) } + return override.newCall(chain.request()).execute() + } + } + + abstract fun clientKey(request: Request): K? + + abstract fun OkHttpClient.Builder.buildForKey(key: K): OkHttpClient +} diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ExperimentalOkHttpApi.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ExperimentalOkHttpApi.kt new file mode 100644 index 000000000000..73a4649479c7 --- /dev/null +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ExperimentalOkHttpApi.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 Block, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okhttp3 + +/** + * Marks declarations that are experimental and subject to change without following SemVer + * conventions. Both binary and source-incompatible changes are possible, including complete removal + * of the experimental API. + * + * Do not use these APIs in modules that may be executed using a version of OkHttp different from + * the version the module was compiled with. + * + * Do not use these APIs in published libraries. + * + * Do not use these APIs if you aren't willing to track changes to them. + */ +@MustBeDocumented +@Retention(value = AnnotationRetention.BINARY) +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.ANNOTATION_CLASS, + AnnotationTarget.PROPERTY, + AnnotationTarget.FIELD, + AnnotationTarget.LOCAL_VARIABLE, + AnnotationTarget.VALUE_PARAMETER, + AnnotationTarget.CONSTRUCTOR, + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.PROPERTY_SETTER, + AnnotationTarget.TYPEALIAS +) +@RequiresOptIn(level = RequiresOptIn.Level.ERROR) +public annotation class ExperimentalOkHttpApi diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/OkHttpClient.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/OkHttpClient.kt index 8af05700e36d..6ab5bcf7d374 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/OkHttpClient.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/OkHttpClient.kt @@ -145,14 +145,6 @@ open class OkHttpClient internal constructor( val interceptors: List = builder.interceptors.toImmutableList() - /** - * Returns an immutable list of Call decorators that have a chance to return a different, likely - * decorating, implementation of Call. This allows functionality such as fail fast without normal Call - * execution based on network conditions, or setting Tracing context on the calling thread. - */ - val callDecorators: List = - builder.callDecorators.toImmutableList() - /** * Returns an immutable list of interceptors that observe a single network request and response. * These interceptors must call [Interceptor.Chain.proceed] exactly once: it is an error for @@ -273,27 +265,6 @@ open class OkHttpClient internal constructor( internal val routeDatabase: RouteDatabase = builder.routeDatabase ?: RouteDatabase() internal val taskRunner: TaskRunner = builder.taskRunner ?: TaskRunner.INSTANCE - private val decoratedCallFactory = - callDecorators.foldRight( - Call.Factory { request -> - RealCall(client = this, originalRequest = request, forWebSocket = false) - }, - ) { callDecorator, next -> - Call.Factory { request -> - callDecorator.newCall( - object : Call.Chain { - override val client: OkHttpClient - get() = this@OkHttpClient - - override val request: Request - get() = request - - override fun proceed(request: Request): Call = next.newCall(request) - }, - ) - } - } - @get:JvmName("connectionPool") val connectionPool: ConnectionPool = builder.connectionPool ?: ConnectionPool().also { @@ -379,7 +350,7 @@ open class OkHttpClient internal constructor( } /** Prepares the [request] to be executed at some point in the future. */ - override fun newCall(request: Request): Call = decoratedCallFactory.newCall(request) + override fun newCall(request: Request): Call = RealCall(this, request, forWebSocket = false) /** Uses [request] to connect a new web socket. */ override fun newWebSocket( @@ -616,7 +587,6 @@ open class OkHttpClient internal constructor( internal var dispatcher: Dispatcher = Dispatcher() internal var connectionPool: ConnectionPool? = null internal val interceptors: MutableList = mutableListOf() - internal val callDecorators: MutableList = mutableListOf() internal val networkInterceptors: MutableList = mutableListOf() internal var eventListenerFactory: EventListener.Factory = EventListener.NONE.asFactory() internal var retryOnConnectionFailure = true @@ -652,7 +622,6 @@ open class OkHttpClient internal constructor( this.dispatcher = okHttpClient.dispatcher this.connectionPool = okHttpClient.connectionPool this.interceptors += okHttpClient.interceptors - this.callDecorators += okHttpClient.callDecorators this.networkInterceptors += okHttpClient.networkInterceptors this.eventListenerFactory = okHttpClient.eventListenerFactory this.retryOnConnectionFailure = okHttpClient.retryOnConnectionFailure @@ -757,11 +726,6 @@ open class OkHttpClient internal constructor( this.eventListenerFactory = eventListenerFactory } - fun addCallDecorator(decorator: Call.Decorator) = - apply { - callDecorators += decorator - } - /** * Configure this client to retry or not when a connectivity problem is encountered. By default, * this client silently recovers from the following problems: diff --git a/okhttp/src/jvmTest/kotlin/okhttp3/CallDecoratorTest.kt b/okhttp/src/jvmTest/kotlin/okhttp3/CallDecoratorTest.kt index 9f4c929f8103..ed044ddb2143 100644 --- a/okhttp/src/jvmTest/kotlin/okhttp3/CallDecoratorTest.kt +++ b/okhttp/src/jvmTest/kotlin/okhttp3/CallDecoratorTest.kt @@ -15,12 +15,13 @@ */ package okhttp3 +import assertk.assertThat +import assertk.assertions.isEqualTo import java.io.IOException import java.util.logging.Logger import mockwebserver3.MockResponse import mockwebserver3.MockWebServer import mockwebserver3.junit5.StartStop -import okhttp3.internal.connection.RealCall import okhttp3.tls.internal.TlsUtil.localhost import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Tag @@ -32,10 +33,9 @@ class CallDecoratorTest { @Suppress("RedundantVisibilityModifier") @JvmField @RegisterExtension - public val clientTestRule = - OkHttpClientTestRule().apply { - logger = Logger.getLogger(CallDecoratorTest::class.java.name) - } + public val clientTestRule = OkHttpClientTestRule().apply { + logger = Logger.getLogger(CallDecoratorTest::class.java.name) + } @StartStop private val server = MockWebServer() @@ -49,11 +49,7 @@ class CallDecoratorTest { val request = Request.Builder().url(server.url("/")).build() val client: OkHttpClient = - clientTestRule - .newClientBuilder() - .enableTls() - .addCallDecorator(AlwaysHttps) - .build() + clientTestRule.newClientBuilder().enableTls().addInterceptor(AlwaysHttps).build() client.newCall(request).execute().use { assertEquals(200, it.code) @@ -64,23 +60,12 @@ class CallDecoratorTest { fun testInsecureRequestChangedToSecure() { server.enqueue(MockResponse()) - val request = - Request - .Builder() - .url( - server - .url("/") - .newBuilder() - .scheme("http") - .build(), - ).build() + val request = Request.Builder().url( + server.url("/").newBuilder().scheme("http").build(), + ).build() val client: OkHttpClient = - clientTestRule - .newClientBuilder() - .enableTls() - .addCallDecorator(AlwaysHttps) - .build() + clientTestRule.newClientBuilder().enableTls().addInterceptor(AlwaysHttps).build() client.newCall(request).execute().use { assertEquals(200, it.code) @@ -88,55 +73,43 @@ class CallDecoratorTest { } } - class WrappedCall( - delegate: Call, - ) : Call by delegate - @Test fun testWrappedCallIsObserved() { server.enqueue(MockResponse()) - val client: OkHttpClient = - clientTestRule - .newClientBuilder() - .addCallDecorator { chain, request -> - // First Call.Decorator will see the result of later decorators - chain.newCall(request).also { - if (it !is WrappedCall) { - throw IOException("expecting wrapped call") - } - if (it.request().tag() != "wrapped") { - throw IOException("expecting tag1") - } - } - }.addCallDecorator { chain, request -> - // Wrap here - val updatedRequest = request.newBuilder().tag("wrapped").build() - WrappedCall(chain.newCall(updatedRequest)) - }.addCallDecorator { chain, request -> - // Updated requests are seen - if (request.tag() != "wrapped") { - throw IOException("expecting tag2") - } - chain.newCall(request).also { - // But Call is RealCall - if (it !is RealCall) { - throw IOException("expecting RealCall") - } - } - }.addInterceptor { chain -> - // Updated requests are seen in interceptors - if (chain.request().tag() != "wrapped") { - throw IOException("expecting tag3") - } - chain.proceed(chain.request()) - }.addNetworkInterceptor { chain -> - // and network interceptors - if (chain.request().tag() != "wrapped") { - throw IOException("expecting tag4") - } - chain.proceed(chain.request()) - }.build() + // TODO reimplement this test for the non Decorator world + + val client: OkHttpClient = clientTestRule.newClientBuilder().addInterceptor({ chain -> + // First Call.Decorator will see the result of later decorators + chain.proceed(chain.request()).also { +// if (it !is WrappedCall) { +// throw IOException("expecting wrapped call") +// } +// assertThat(chain.request().tag()).isEqualTo("wrapped") + } + }).addInterceptor({ chain -> + // Wrap here + val updatedRequest = chain.request().newBuilder().tag("wrapped").build() + chain.proceed(updatedRequest) + }).addInterceptor({ chain -> + // Updated requests are seen + assertThat(chain.request().tag()).isEqualTo("wrapped") + chain.proceed(chain.request()) +// .also { + // But Call is RealCall +// if (it !is RealCall) { +// throw IOException("expecting RealCall") +// } +// } + }).addInterceptor { chain -> + // Updated requests are seen in interceptors + assertThat(chain.request().tag()).isEqualTo("wrapped") + chain.proceed(chain.request()) + }.addNetworkInterceptor { chain -> + // and network interceptors + assertThat(chain.request().tag()).isEqualTo("wrapped") + chain.proceed(chain.request()) + }.build() val originalRequest = Request.Builder().url(server.url("/")).build() client.newCall(originalRequest).execute().use { @@ -144,24 +117,24 @@ class CallDecoratorTest { } } + @OptIn(ExperimentalOkHttpApi::class) @Test fun testCanShortCircuit() { server.enqueue(MockResponse()) val request = Request.Builder().url(server.url("/")).build() - val client: OkHttpClient = - clientTestRule - .newClientBuilder() - .build() + val client: OkHttpClient = clientTestRule.newClientBuilder().build() val redirectingClient: OkHttpClient = - clientTestRule - .newClientBuilder() - .addCallDecorator { _, request -> - // Use the other client - client.newCall(request) - }.addInterceptor { + clientTestRule.newClientBuilder().addInterceptor(object : ClientForkingInterceptor() { + override fun clientKey(request: Request) = Unit + + override fun OkHttpClient.Builder.buildForKey(key: Unit): OkHttpClient { + // fully override + return client + } + }).addInterceptor { // Fail if we get here throw IOException("You shall not pass") }.build() @@ -180,25 +153,17 @@ class CallDecoratorTest { } } -private object AlwaysHttps : Call.Decorator { - override fun newCall( - chain: Call.Factory, - request: Request, - ): Call { - val updatedRequest = - if (request.url.scheme == "http") { - request - .newBuilder() - .url( - request.url - .newBuilder() - .scheme("https") - .build(), - ).build() - } else { - request - } - - return chain.newCall(updatedRequest) +private object AlwaysHttps : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val updatedRequest = if (request.url.scheme == "http") { + request.newBuilder().url( + request.url.newBuilder().scheme("https").build(), + ).build() + } else { + request + } + + return chain.proceed(updatedRequest) } } diff --git a/settings.gradle.kts b/settings.gradle.kts index f78afa259afd..6b8650606bb7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -64,9 +64,9 @@ val localProperties = Properties().apply { } } val sdkDir = localProperties.getProperty("sdk.dir") -if (androidHome != null || sdkDir != null) { +//if (androidHome != null || sdkDir != null) { include(":android-test") include(":android-test-app") -} +//} enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") From 393a96135c9e907357d7cce63b6c819bcda61b5b Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sun, 12 Oct 2025 09:46:55 +0100 Subject: [PATCH 3/4] cleanup --- .../java/okhttp/android/test/AlwaysHttps.kt | 77 -------- .../android/test/AndroidCallDecoratorTest.kt | 100 ----------- .../android/test/AndroidNetworkPinningTest.kt | 3 +- .../java/okhttp/android/test/OffMainThread.kt | 41 ----- okhttp/api/android/okhttp.api | 32 ++-- okhttp/api/jvm/okhttp.api | 23 ++- .../okhttp3/android/AndroidNetworkPinning.kt | 6 +- .../okhttp3/ClientForkingInterceptor.kt | 3 +- .../kotlin/okhttp3/ExperimentalOkHttpApi.kt | 2 +- .../kotlin/okhttp3/CallDecoratorTest.kt | 169 ------------------ 10 files changed, 38 insertions(+), 418 deletions(-) delete mode 100644 android-test/src/androidTest/java/okhttp/android/test/AlwaysHttps.kt delete mode 100644 android-test/src/androidTest/java/okhttp/android/test/AndroidCallDecoratorTest.kt delete mode 100644 android-test/src/androidTest/java/okhttp/android/test/OffMainThread.kt delete mode 100644 okhttp/src/jvmTest/kotlin/okhttp3/CallDecoratorTest.kt diff --git a/android-test/src/androidTest/java/okhttp/android/test/AlwaysHttps.kt b/android-test/src/androidTest/java/okhttp/android/test/AlwaysHttps.kt deleted file mode 100644 index 2ec97da828b3..000000000000 --- a/android-test/src/androidTest/java/okhttp/android/test/AlwaysHttps.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (C) 2025 Block, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package okhttp.android.test - -import android.os.Build -import android.security.NetworkSecurityPolicy -import okhttp3.Call -import okhttp3.Interceptor -import okhttp3.Request -import okhttp3.Response - -class AlwaysHttps( - policy: Policy, -) : Interceptor { - val hostPolicy: HostPolicy = policy.hostPolicy - - override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request() - - val updatedRequest = - if (request.url.scheme == "http" && !hostPolicy.isCleartextTrafficPermitted(request)) { - request - .newBuilder() - .url( - request.url - .newBuilder() - .scheme("https") - .build(), - ).build() - } else { - request - } - - return chain.proceed(updatedRequest) - } - - fun interface HostPolicy { - fun isCleartextTrafficPermitted(request: Request): Boolean - } - - enum class Policy { - Always { - override val hostPolicy: HostPolicy - get() = HostPolicy { false } - }, - Manifest { - override val hostPolicy: HostPolicy - get() = - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) { - val networkSecurityPolicy = NetworkSecurityPolicy.getInstance() - - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) { - HostPolicy { networkSecurityPolicy.isCleartextTrafficPermitted(it.url.host) } - } else { - HostPolicy { networkSecurityPolicy.isCleartextTrafficPermitted } - } - } else { - HostPolicy { true } - } - }, ; - - abstract val hostPolicy: HostPolicy - } -} diff --git a/android-test/src/androidTest/java/okhttp/android/test/AndroidCallDecoratorTest.kt b/android-test/src/androidTest/java/okhttp/android/test/AndroidCallDecoratorTest.kt deleted file mode 100644 index 9f062fc01354..000000000000 --- a/android-test/src/androidTest/java/okhttp/android/test/AndroidCallDecoratorTest.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (C) 2025 Block, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package okhttp.android.test - -import java.util.logging.Logger -import mockwebserver3.MockResponse -import mockwebserver3.MockWebServer -import mockwebserver3.junit5.StartStop -import okhttp.android.test.AlwaysHttps.Policy -import okhttp3.OkHttpClient -import okhttp3.OkHttpClientTestRule -import okhttp3.Request -import okhttp3.tls.internal.TlsUtil.localhost -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Tag -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.RegisterExtension - -@Tag("Slow") -class AndroidCallDecoratorTest { - @Suppress("RedundantVisibilityModifier") - @JvmField - @RegisterExtension - public val clientTestRule = - OkHttpClientTestRule().apply { - logger = Logger.getLogger(AndroidCallDecoratorTest::class.java.name) - } - - private var client: OkHttpClient = - clientTestRule - .newClientBuilder() - .addCallDecorator(AlwaysHttps(Policy.Always)) - .addCallDecorator(OffMainThread) - .build() - - @StartStop - private val server = MockWebServer() - - private val handshakeCertificates = localhost() - - @Test - fun testSecureRequest() { - enableTls() - - server.enqueue(MockResponse()) - - val request = Request.Builder().url(server.url("/")).build() - - client.newCall(request).execute().use { - assertEquals(200, it.code) - } - } - - @Test - fun testInsecureRequestChangedToSecure() { - enableTls() - - server.enqueue(MockResponse()) - - val request = - Request - .Builder() - .url( - server - .url("/") - .newBuilder() - .scheme("http") - .build(), - ).build() - - client.newCall(request).execute().use { - assertEquals(200, it.code) - assertEquals("https", it.request.url.scheme) - } - } - - private fun enableTls() { - client = - client - .newBuilder() - .sslSocketFactory( - handshakeCertificates.sslSocketFactory(), - handshakeCertificates.trustManager, - ).build() - server.useHttps(handshakeCertificates.sslSocketFactory()) - } -} diff --git a/android-test/src/androidTest/java/okhttp/android/test/AndroidNetworkPinningTest.kt b/android-test/src/androidTest/java/okhttp/android/test/AndroidNetworkPinningTest.kt index d5c9a8b50fa8..4b35857ef21b 100644 --- a/android-test/src/androidTest/java/okhttp/android/test/AndroidNetworkPinningTest.kt +++ b/android-test/src/androidTest/java/okhttp/android/test/AndroidNetworkPinningTest.kt @@ -58,7 +58,8 @@ class AndroidNetworkPinningTest { .addInterceptor(pinning) .addInterceptor { it.proceed( - it.request() + it + .request() .newBuilder() .header("second-decorator", "true") .build(), diff --git a/android-test/src/androidTest/java/okhttp/android/test/OffMainThread.kt b/android-test/src/androidTest/java/okhttp/android/test/OffMainThread.kt deleted file mode 100644 index 88d5401b8d83..000000000000 --- a/android-test/src/androidTest/java/okhttp/android/test/OffMainThread.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2025 Block, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package okhttp.android.test - -import android.os.Looper -import okhttp3.Call -import okhttp3.Response - -/** - * Sample of a Decorator that will fail any call on the Android Main thread. - */ -object OffMainThread : Interceptor { - override fun newCall(chain: Call.Chain): Call = StrictModeCall(chain.proceed(chain.request)) - - private class StrictModeCall( - private val delegate: Call, - ) : Call by delegate { - override fun execute(): Response { - if (Looper.getMainLooper() === Looper.myLooper()) { - throw IllegalStateException("Network on main thread") - } - - return delegate.execute() - } - - override fun clone(): Call = StrictModeCall(delegate.clone()) - } -} diff --git a/okhttp/api/android/okhttp.api b/okhttp/api/android/okhttp.api index 78f4016d1a7a..504a69bc0fd7 100644 --- a/okhttp/api/android/okhttp.api +++ b/okhttp/api/android/okhttp.api @@ -129,16 +129,6 @@ public abstract interface class okhttp3/Call : java/lang/Cloneable { public abstract fun timeout ()Lokio/Timeout; } -public abstract interface class okhttp3/Call$Chain { - public abstract fun getClient ()Lokhttp3/OkHttpClient; - public abstract fun getRequest ()Lokhttp3/Request; - public abstract fun proceed (Lokhttp3/Request;)Lokhttp3/Call; -} - -public abstract interface class okhttp3/Call$Decorator { - public abstract fun newCall (Lokhttp3/Call$Chain;)Lokhttp3/Call; -} - public abstract interface class okhttp3/Call$Factory { public abstract fun newCall (Lokhttp3/Request;)Lokhttp3/Call; } @@ -336,6 +326,14 @@ public final class okhttp3/CipherSuite$Companion { public final fun forJavaName (Ljava/lang/String;)Lokhttp3/CipherSuite; } +public abstract class okhttp3/ClientForkingInterceptor : okhttp3/Interceptor { + public fun ()V + public abstract fun buildForKey (Lokhttp3/OkHttpClient$Builder;Ljava/lang/Object;)Lokhttp3/OkHttpClient; + public abstract fun clientKey (Lokhttp3/Request;)Ljava/lang/Object; + public fun intercept (Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; + public final fun removeClient (Ljava/lang/Object;)V +} + public class okhttp3/CompressionInterceptor : okhttp3/Interceptor { public fun ([Lokhttp3/CompressionInterceptor$DecompressionAlgorithm;)V public final fun getAlgorithms ()[Lokhttp3/CompressionInterceptor$DecompressionAlgorithm; @@ -537,6 +535,9 @@ public abstract interface class okhttp3/EventListener$Factory { public abstract fun create (Lokhttp3/Call;)Lokhttp3/EventListener; } +public abstract interface annotation class okhttp3/ExperimentalOkHttpApi : java/lang/annotation/Annotation { +} + public final class okhttp3/FormBody : okhttp3/RequestBody { public static final field Companion Lokhttp3/FormBody$Companion; public final fun -deprecated_size ()I @@ -915,7 +916,6 @@ public class okhttp3/OkHttpClient : okhttp3/Call$Factory, okhttp3/WebSocket$Fact public final fun fastFallback ()Z public final fun followRedirects ()Z public final fun followSslRedirects ()Z - public final fun getCallDecorators ()Ljava/util/List; public final fun hostnameVerifier ()Ljavax/net/ssl/HostnameVerifier; public final fun interceptors ()Ljava/util/List; public final fun minWebSocketMessageToCompress ()J @@ -941,7 +941,6 @@ public final class okhttp3/OkHttpClient$Builder { public final fun -addInterceptor (Lkotlin/jvm/functions/Function1;)Lokhttp3/OkHttpClient$Builder; public final fun -addNetworkInterceptor (Lkotlin/jvm/functions/Function1;)Lokhttp3/OkHttpClient$Builder; public fun ()V - public final fun addCallDecorator (Lokhttp3/Call$Decorator;)Lokhttp3/OkHttpClient$Builder; public final fun addInterceptor (Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient$Builder; public final fun addNetworkInterceptor (Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient$Builder; public final fun authenticator (Lokhttp3/Authenticator;)Lokhttp3/OkHttpClient$Builder; @@ -1293,3 +1292,12 @@ public abstract class okhttp3/WebSocketListener { public fun onOpen (Lokhttp3/WebSocket;Lokhttp3/Response;)V } +public final class okhttp3/android/AndroidNetworkPinning : okhttp3/ClientForkingInterceptor { + public fun ()V + public fun buildForKey (Lokhttp3/OkHttpClient$Builder;Landroid/net/Network;)Lokhttp3/OkHttpClient; + public synthetic fun buildForKey (Lokhttp3/OkHttpClient$Builder;Ljava/lang/Object;)Lokhttp3/OkHttpClient; + public fun clientKey (Lokhttp3/Request;)Landroid/net/Network; + public synthetic fun clientKey (Lokhttp3/Request;)Ljava/lang/Object; + public final fun getNetworkCallback ()Landroid/net/ConnectivityManager$NetworkCallback; +} + diff --git a/okhttp/api/jvm/okhttp.api b/okhttp/api/jvm/okhttp.api index 498335764aad..f534ae4ca035 100644 --- a/okhttp/api/jvm/okhttp.api +++ b/okhttp/api/jvm/okhttp.api @@ -129,16 +129,6 @@ public abstract interface class okhttp3/Call : java/lang/Cloneable { public abstract fun timeout ()Lokio/Timeout; } -public abstract interface class okhttp3/Call$Chain { - public abstract fun getClient ()Lokhttp3/OkHttpClient; - public abstract fun getRequest ()Lokhttp3/Request; - public abstract fun proceed (Lokhttp3/Request;)Lokhttp3/Call; -} - -public abstract interface class okhttp3/Call$Decorator { - public abstract fun newCall (Lokhttp3/Call$Chain;)Lokhttp3/Call; -} - public abstract interface class okhttp3/Call$Factory { public abstract fun newCall (Lokhttp3/Request;)Lokhttp3/Call; } @@ -336,6 +326,14 @@ public final class okhttp3/CipherSuite$Companion { public final fun forJavaName (Ljava/lang/String;)Lokhttp3/CipherSuite; } +public abstract class okhttp3/ClientForkingInterceptor : okhttp3/Interceptor { + public fun ()V + public abstract fun buildForKey (Lokhttp3/OkHttpClient$Builder;Ljava/lang/Object;)Lokhttp3/OkHttpClient; + public abstract fun clientKey (Lokhttp3/Request;)Ljava/lang/Object; + public fun intercept (Lokhttp3/Interceptor$Chain;)Lokhttp3/Response; + public final fun removeClient (Ljava/lang/Object;)V +} + public class okhttp3/CompressionInterceptor : okhttp3/Interceptor { public fun ([Lokhttp3/CompressionInterceptor$DecompressionAlgorithm;)V public final fun getAlgorithms ()[Lokhttp3/CompressionInterceptor$DecompressionAlgorithm; @@ -537,6 +535,9 @@ public abstract interface class okhttp3/EventListener$Factory { public abstract fun create (Lokhttp3/Call;)Lokhttp3/EventListener; } +public abstract interface annotation class okhttp3/ExperimentalOkHttpApi : java/lang/annotation/Annotation { +} + public final class okhttp3/FormBody : okhttp3/RequestBody { public static final field Companion Lokhttp3/FormBody$Companion; public final fun -deprecated_size ()I @@ -914,7 +915,6 @@ public class okhttp3/OkHttpClient : okhttp3/Call$Factory, okhttp3/WebSocket$Fact public final fun fastFallback ()Z public final fun followRedirects ()Z public final fun followSslRedirects ()Z - public final fun getCallDecorators ()Ljava/util/List; public final fun hostnameVerifier ()Ljavax/net/ssl/HostnameVerifier; public final fun interceptors ()Ljava/util/List; public final fun minWebSocketMessageToCompress ()J @@ -940,7 +940,6 @@ public final class okhttp3/OkHttpClient$Builder { public final fun -addInterceptor (Lkotlin/jvm/functions/Function1;)Lokhttp3/OkHttpClient$Builder; public final fun -addNetworkInterceptor (Lkotlin/jvm/functions/Function1;)Lokhttp3/OkHttpClient$Builder; public fun ()V - public final fun addCallDecorator (Lokhttp3/Call$Decorator;)Lokhttp3/OkHttpClient$Builder; public final fun addInterceptor (Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient$Builder; public final fun addNetworkInterceptor (Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient$Builder; public final fun authenticator (Lokhttp3/Authenticator;)Lokhttp3/OkHttpClient$Builder; diff --git a/okhttp/src/androidMain/kotlin/okhttp3/android/AndroidNetworkPinning.kt b/okhttp/src/androidMain/kotlin/okhttp3/android/AndroidNetworkPinning.kt index ed99a2c6b326..32486f3b4c09 100644 --- a/okhttp/src/androidMain/kotlin/okhttp3/android/AndroidNetworkPinning.kt +++ b/okhttp/src/androidMain/kotlin/okhttp3/android/AndroidNetworkPinning.kt @@ -33,7 +33,6 @@ import okhttp3.internal.SuppressSignatureCheck @RequiresApi(Build.VERSION_CODES.Q) @SuppressSignatureCheck class AndroidNetworkPinning : ClientForkingInterceptor() { - /** ConnectivityManager.NetworkCallback that will clean up after networks are lost. */ val networkCallback = object : ConnectivityManager.NetworkCallback() { @@ -42,14 +41,13 @@ class AndroidNetworkPinning : ClientForkingInterceptor() { } } - override fun OkHttpClient.Builder.buildForKey(key: Network): OkHttpClient { - return dns(AndroidDns(key)) + override fun OkHttpClient.Builder.buildForKey(key: Network): OkHttpClient = + dns(AndroidDns(key)) .socketFactory(key.socketFactory) .apply { // Keep interceptors after this one in the new client interceptors.subList(interceptors.indexOf(this@AndroidNetworkPinning) + 1, interceptors.size).clear() }.build() - } override fun clientKey(request: Request): Network? = request.tag() } diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ClientForkingInterceptor.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ClientForkingInterceptor.kt index 5e845e0d3677..af5f185db76b 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ClientForkingInterceptor.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ClientForkingInterceptor.kt @@ -11,7 +11,8 @@ abstract class ClientForkingInterceptor : Interceptor { // TODO consider caching by client and cleaning up private val forkedClients = ConcurrentHashMap() - // TODO consider whether we need to address lifecycle for cleanup of clients + // TODO consider whether we need to address lifecycle of clients + // If someone else forks this client, should we know that we need a different pool? // override fun onNewClientInstance(client: OkHttpClient): Interceptor { // return this // } diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ExperimentalOkHttpApi.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ExperimentalOkHttpApi.kt index 73a4649479c7..082eeeec677e 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ExperimentalOkHttpApi.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/ExperimentalOkHttpApi.kt @@ -40,7 +40,7 @@ package okhttp3 AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER, - AnnotationTarget.TYPEALIAS + AnnotationTarget.TYPEALIAS, ) @RequiresOptIn(level = RequiresOptIn.Level.ERROR) public annotation class ExperimentalOkHttpApi diff --git a/okhttp/src/jvmTest/kotlin/okhttp3/CallDecoratorTest.kt b/okhttp/src/jvmTest/kotlin/okhttp3/CallDecoratorTest.kt deleted file mode 100644 index ed044ddb2143..000000000000 --- a/okhttp/src/jvmTest/kotlin/okhttp3/CallDecoratorTest.kt +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright (C) 2025 Block, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package okhttp3 - -import assertk.assertThat -import assertk.assertions.isEqualTo -import java.io.IOException -import java.util.logging.Logger -import mockwebserver3.MockResponse -import mockwebserver3.MockWebServer -import mockwebserver3.junit5.StartStop -import okhttp3.tls.internal.TlsUtil.localhost -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Tag -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.RegisterExtension - -@Tag("Slow") -class CallDecoratorTest { - @Suppress("RedundantVisibilityModifier") - @JvmField - @RegisterExtension - public val clientTestRule = OkHttpClientTestRule().apply { - logger = Logger.getLogger(CallDecoratorTest::class.java.name) - } - - @StartStop - private val server = MockWebServer() - - private val handshakeCertificates = localhost() - - @Test - fun testSecureRequest() { - server.enqueue(MockResponse()) - - val request = Request.Builder().url(server.url("/")).build() - - val client: OkHttpClient = - clientTestRule.newClientBuilder().enableTls().addInterceptor(AlwaysHttps).build() - - client.newCall(request).execute().use { - assertEquals(200, it.code) - } - } - - @Test - fun testInsecureRequestChangedToSecure() { - server.enqueue(MockResponse()) - - val request = Request.Builder().url( - server.url("/").newBuilder().scheme("http").build(), - ).build() - - val client: OkHttpClient = - clientTestRule.newClientBuilder().enableTls().addInterceptor(AlwaysHttps).build() - - client.newCall(request).execute().use { - assertEquals(200, it.code) - assertEquals("https", it.request.url.scheme) - } - } - - @Test - fun testWrappedCallIsObserved() { - server.enqueue(MockResponse()) - - // TODO reimplement this test for the non Decorator world - - val client: OkHttpClient = clientTestRule.newClientBuilder().addInterceptor({ chain -> - // First Call.Decorator will see the result of later decorators - chain.proceed(chain.request()).also { -// if (it !is WrappedCall) { -// throw IOException("expecting wrapped call") -// } -// assertThat(chain.request().tag()).isEqualTo("wrapped") - } - }).addInterceptor({ chain -> - // Wrap here - val updatedRequest = chain.request().newBuilder().tag("wrapped").build() - chain.proceed(updatedRequest) - }).addInterceptor({ chain -> - // Updated requests are seen - assertThat(chain.request().tag()).isEqualTo("wrapped") - chain.proceed(chain.request()) -// .also { - // But Call is RealCall -// if (it !is RealCall) { -// throw IOException("expecting RealCall") -// } -// } - }).addInterceptor { chain -> - // Updated requests are seen in interceptors - assertThat(chain.request().tag()).isEqualTo("wrapped") - chain.proceed(chain.request()) - }.addNetworkInterceptor { chain -> - // and network interceptors - assertThat(chain.request().tag()).isEqualTo("wrapped") - chain.proceed(chain.request()) - }.build() - - val originalRequest = Request.Builder().url(server.url("/")).build() - client.newCall(originalRequest).execute().use { - assertEquals(200, it.code) - } - } - - @OptIn(ExperimentalOkHttpApi::class) - @Test - fun testCanShortCircuit() { - server.enqueue(MockResponse()) - - val request = Request.Builder().url(server.url("/")).build() - - val client: OkHttpClient = clientTestRule.newClientBuilder().build() - - val redirectingClient: OkHttpClient = - clientTestRule.newClientBuilder().addInterceptor(object : ClientForkingInterceptor() { - override fun clientKey(request: Request) = Unit - - override fun OkHttpClient.Builder.buildForKey(key: Unit): OkHttpClient { - // fully override - return client - } - }).addInterceptor { - // Fail if we get here - throw IOException("You shall not pass") - }.build() - - redirectingClient.newCall(request).execute().use { - assertEquals(200, it.code) - } - } - - private fun OkHttpClient.Builder.enableTls(): OkHttpClient.Builder { - server.useHttps(handshakeCertificates.sslSocketFactory()) - return sslSocketFactory( - handshakeCertificates.sslSocketFactory(), - handshakeCertificates.trustManager, - ) - } -} - -private object AlwaysHttps : Interceptor { - override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request() - val updatedRequest = if (request.url.scheme == "http") { - request.newBuilder().url( - request.url.newBuilder().scheme("https").build(), - ).build() - } else { - request - } - - return chain.proceed(updatedRequest) - } -} From fd2a0c5efc271473ad4b3088a967fe59eb9e3381 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sun, 12 Oct 2025 09:47:58 +0100 Subject: [PATCH 4/4] revert --- settings.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 6b8650606bb7..f78afa259afd 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -64,9 +64,9 @@ val localProperties = Properties().apply { } } val sdkDir = localProperties.getProperty("sdk.dir") -//if (androidHome != null || sdkDir != null) { +if (androidHome != null || sdkDir != null) { include(":android-test") include(":android-test-app") -//} +} enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")