Skip to content

Commit f39a193

Browse files
committed
Call decorator
1 parent 5eb71c8 commit f39a193

File tree

9 files changed

+511
-1
lines changed

9 files changed

+511
-1
lines changed

android-test/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ dependencies {
6565
"friendsImplementation"(projects.okhttpDnsoverhttps)
6666

6767
testImplementation(projects.okhttp)
68+
testImplementation(projects.okhttpCoroutines)
6869
testImplementation(libs.junit)
6970
testImplementation(libs.junit.ktx)
7071
testImplementation(libs.assertk)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright (C) 2025 Block, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package okhttp.android.test
17+
18+
import android.os.Build
19+
import android.security.NetworkSecurityPolicy
20+
import okhttp3.Call
21+
import okhttp3.Request
22+
23+
class AlwaysHttps(
24+
policy: Policy,
25+
) : Call.Decorator {
26+
val hostPolicy: HostPolicy = policy.hostPolicy
27+
28+
override fun newCall(chain: Call.Chain): Call {
29+
val request = chain.request
30+
31+
val updatedRequest =
32+
if (request.url.scheme == "http" && !hostPolicy.isCleartextTrafficPermitted(request)) {
33+
request
34+
.newBuilder()
35+
.url(
36+
request.url
37+
.newBuilder()
38+
.scheme("https")
39+
.build(),
40+
).build()
41+
} else {
42+
request
43+
}
44+
45+
return chain.proceed(updatedRequest)
46+
}
47+
48+
fun interface HostPolicy {
49+
fun isCleartextTrafficPermitted(request: Request): Boolean
50+
}
51+
52+
enum class Policy {
53+
Always {
54+
override val hostPolicy: HostPolicy
55+
get() = HostPolicy { false }
56+
},
57+
Manifest {
58+
override val hostPolicy: HostPolicy
59+
get() =
60+
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
61+
val networkSecurityPolicy = NetworkSecurityPolicy.getInstance()
62+
63+
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.N) {
64+
HostPolicy { networkSecurityPolicy.isCleartextTrafficPermitted(it.url.host) }
65+
} else {
66+
HostPolicy { networkSecurityPolicy.isCleartextTrafficPermitted }
67+
}
68+
} else {
69+
HostPolicy { true }
70+
}
71+
}, ;
72+
73+
abstract val hostPolicy: HostPolicy
74+
}
75+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Copyright (C) 2025 Block, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package okhttp.android.test
17+
18+
import java.util.logging.Logger
19+
import mockwebserver3.MockResponse
20+
import mockwebserver3.MockWebServer
21+
import mockwebserver3.junit5.StartStop
22+
import okhttp.android.test.AlwaysHttps.Policy
23+
import okhttp3.OkHttpClient
24+
import okhttp3.OkHttpClientTestRule
25+
import okhttp3.Request
26+
import okhttp3.tls.internal.TlsUtil.localhost
27+
import org.junit.jupiter.api.Assertions.assertEquals
28+
import org.junit.jupiter.api.Tag
29+
import org.junit.jupiter.api.Test
30+
import org.junit.jupiter.api.extension.RegisterExtension
31+
32+
@Tag("Slow")
33+
class AndroidCallDecoratorTest {
34+
@Suppress("RedundantVisibilityModifier")
35+
@JvmField
36+
@RegisterExtension
37+
public val clientTestRule =
38+
OkHttpClientTestRule().apply {
39+
logger = Logger.getLogger(AndroidCallDecoratorTest::class.java.name)
40+
}
41+
42+
private var client: OkHttpClient =
43+
clientTestRule
44+
.newClientBuilder()
45+
.addCallDecorator(AlwaysHttps(Policy.Always))
46+
.addCallDecorator(OffMainThread)
47+
.build()
48+
49+
@StartStop
50+
private val server = MockWebServer()
51+
52+
private val handshakeCertificates = localhost()
53+
54+
@Test
55+
fun testSecureRequest() {
56+
enableTls()
57+
58+
server.enqueue(MockResponse())
59+
60+
val request = Request.Builder().url(server.url("/")).build()
61+
62+
client.newCall(request).execute().use {
63+
assertEquals(200, it.code)
64+
}
65+
}
66+
67+
@Test
68+
fun testInsecureRequestChangedToSecure() {
69+
enableTls()
70+
71+
server.enqueue(MockResponse())
72+
73+
val request =
74+
Request
75+
.Builder()
76+
.url(
77+
server
78+
.url("/")
79+
.newBuilder()
80+
.scheme("http")
81+
.build(),
82+
).build()
83+
84+
client.newCall(request).execute().use {
85+
assertEquals(200, it.code)
86+
assertEquals("https", it.request.url.scheme)
87+
}
88+
}
89+
90+
private fun enableTls() {
91+
client =
92+
client
93+
.newBuilder()
94+
.sslSocketFactory(
95+
handshakeCertificates.sslSocketFactory(),
96+
handshakeCertificates.trustManager,
97+
).build()
98+
server.useHttps(handshakeCertificates.sslSocketFactory())
99+
}
100+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright (C) 2025 Block, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package okhttp.android.test
17+
18+
import android.os.Looper
19+
import okhttp3.Call
20+
import okhttp3.Response
21+
22+
/**
23+
* Sample of a Decorator that will fail any call on the Android Main thread.
24+
*/
25+
object OffMainThread : Call.Decorator {
26+
override fun newCall(chain: Call.Chain): Call = StrictModeCall(chain.proceed(chain.request))
27+
28+
private class StrictModeCall(
29+
private val delegate: Call,
30+
) : Call by delegate {
31+
override fun execute(): Response {
32+
if (Looper.getMainLooper() === Looper.myLooper()) {
33+
throw IllegalStateException("Network on main thread")
34+
}
35+
36+
return delegate.execute()
37+
}
38+
39+
override fun clone(): Call = StrictModeCall(delegate.clone())
40+
}
41+
}

okhttp/api/android/okhttp.api

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,16 @@ public abstract interface class okhttp3/Call : java/lang/Cloneable {
129129
public abstract fun timeout ()Lokio/Timeout;
130130
}
131131

132+
public abstract interface class okhttp3/Call$Chain {
133+
public abstract fun getClient ()Lokhttp3/OkHttpClient;
134+
public abstract fun getRequest ()Lokhttp3/Request;
135+
public abstract fun proceed (Lokhttp3/Request;)Lokhttp3/Call;
136+
}
137+
138+
public abstract interface class okhttp3/Call$Decorator {
139+
public abstract fun newCall (Lokhttp3/Call$Chain;)Lokhttp3/Call;
140+
}
141+
132142
public abstract interface class okhttp3/Call$Factory {
133143
public abstract fun newCall (Lokhttp3/Request;)Lokhttp3/Call;
134144
}
@@ -902,6 +912,7 @@ public class okhttp3/OkHttpClient : okhttp3/Call$Factory, okhttp3/WebSocket$Fact
902912
public final fun fastFallback ()Z
903913
public final fun followRedirects ()Z
904914
public final fun followSslRedirects ()Z
915+
public final fun getCallDecorators ()Ljava/util/List;
905916
public final fun hostnameVerifier ()Ljavax/net/ssl/HostnameVerifier;
906917
public final fun interceptors ()Ljava/util/List;
907918
public final fun minWebSocketMessageToCompress ()J
@@ -927,6 +938,7 @@ public final class okhttp3/OkHttpClient$Builder {
927938
public final fun -addInterceptor (Lkotlin/jvm/functions/Function1;)Lokhttp3/OkHttpClient$Builder;
928939
public final fun -addNetworkInterceptor (Lkotlin/jvm/functions/Function1;)Lokhttp3/OkHttpClient$Builder;
929940
public fun <init> ()V
941+
public final fun addCallDecorator (Lokhttp3/Call$Decorator;)Lokhttp3/OkHttpClient$Builder;
930942
public final fun addInterceptor (Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient$Builder;
931943
public final fun addNetworkInterceptor (Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient$Builder;
932944
public final fun authenticator (Lokhttp3/Authenticator;)Lokhttp3/OkHttpClient$Builder;

okhttp/api/jvm/okhttp.api

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,16 @@ public abstract interface class okhttp3/Call : java/lang/Cloneable {
129129
public abstract fun timeout ()Lokio/Timeout;
130130
}
131131

132+
public abstract interface class okhttp3/Call$Chain {
133+
public abstract fun getClient ()Lokhttp3/OkHttpClient;
134+
public abstract fun getRequest ()Lokhttp3/Request;
135+
public abstract fun proceed (Lokhttp3/Request;)Lokhttp3/Call;
136+
}
137+
138+
public abstract interface class okhttp3/Call$Decorator {
139+
public abstract fun newCall (Lokhttp3/Call$Chain;)Lokhttp3/Call;
140+
}
141+
132142
public abstract interface class okhttp3/Call$Factory {
133143
public abstract fun newCall (Lokhttp3/Request;)Lokhttp3/Call;
134144
}
@@ -901,6 +911,7 @@ public class okhttp3/OkHttpClient : okhttp3/Call$Factory, okhttp3/WebSocket$Fact
901911
public final fun fastFallback ()Z
902912
public final fun followRedirects ()Z
903913
public final fun followSslRedirects ()Z
914+
public final fun getCallDecorators ()Ljava/util/List;
904915
public final fun hostnameVerifier ()Ljavax/net/ssl/HostnameVerifier;
905916
public final fun interceptors ()Ljava/util/List;
906917
public final fun minWebSocketMessageToCompress ()J
@@ -926,6 +937,7 @@ public final class okhttp3/OkHttpClient$Builder {
926937
public final fun -addInterceptor (Lkotlin/jvm/functions/Function1;)Lokhttp3/OkHttpClient$Builder;
927938
public final fun -addNetworkInterceptor (Lkotlin/jvm/functions/Function1;)Lokhttp3/OkHttpClient$Builder;
928939
public fun <init> ()V
940+
public final fun addCallDecorator (Lokhttp3/Call$Decorator;)Lokhttp3/OkHttpClient$Builder;
929941
public final fun addInterceptor (Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient$Builder;
930942
public final fun addNetworkInterceptor (Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient$Builder;
931943
public final fun authenticator (Lokhttp3/Authenticator;)Lokhttp3/OkHttpClient$Builder;

okhttp/src/commonJvmAndroid/kotlin/okhttp3/Call.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,33 @@ interface Call : Cloneable {
9696
fun interface Factory {
9797
fun newCall(request: Request): Call
9898
}
99+
100+
/**
101+
* The equivalent of an Interceptor for [Call.Factory], but supported directly within [OkHttpClient] newCall.
102+
*
103+
* An [Interceptor] forms a chain as part of execution of a Call. Instead, Call.Decorator intercepts
104+
* [Call.Factory.newCall] with similar flexibility to Application [OkHttpClient.interceptors].
105+
*
106+
* That is, it may do any of
107+
* - Modify the request such as adding Tracing Context
108+
* - Wrap the [Call] returned
109+
* - Return some [Call] implementation that will immediately fail avoiding network calls based on network or
110+
* authentication state.
111+
* - Redirect the [Call], such as using an alternative [Call.Factory].
112+
* - Defer execution, something not safe in an Interceptor.
113+
*
114+
* It should not throw an exception, instead it should return a Call that will fail on [Call.execute].
115+
*
116+
* A Decorator that changes the OkHttpClient should typically retain later decorators in the new client.
117+
*/
118+
fun interface Decorator {
119+
fun newCall(chain: Chain): Call
120+
}
121+
122+
interface Chain {
123+
val client: OkHttpClient
124+
val request: Request
125+
126+
fun proceed(request: Request): Call
127+
}
99128
}

0 commit comments

Comments
 (0)