Skip to content

Commit e7d70c5

Browse files
authored
Remove existing interceptors from ApolloClient.Builder before adding new ones (#5858)
* Remove existing ApolloInterceptors when calling ApolloClient.Builder.httpCache() * Remove existing ApolloInterceptors when calling ApolloClient.Builder.autoPersistedQueries() * Remove existing HttpInterceptors when calling ApolloClient.Builder.httpCache() * Remove existing HttpInterceptors when calling ApolloClient.Builder.httpBatching() * Add a test for httpCache * Don't crash if no `X-APOLLO-SERVED-DATE` header is present * Revert CachingHttpInterceptor change
1 parent 0a4ce29 commit e7d70c5

File tree

7 files changed

+177
-88
lines changed

7 files changed

+177
-88
lines changed

libraries/apollo-http-cache/src/main/kotlin/com/apollographql/apollo3/cache/http/HttpCacheExtensions.kt

Lines changed: 16 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,16 @@
33
package com.apollographql.apollo3.cache.http
44

55
import com.apollographql.apollo3.ApolloClient
6-
import com.apollographql.apollo3.api.ApolloRequest
76
import com.apollographql.apollo3.api.ApolloResponse
87
import com.apollographql.apollo3.api.ExecutionContext
98
import com.apollographql.apollo3.api.MutableExecutionOptions
10-
import com.apollographql.apollo3.api.Mutation
119
import com.apollographql.apollo3.api.Operation
12-
import com.apollographql.apollo3.api.Query
13-
import com.apollographql.apollo3.api.Subscription
14-
import com.apollographql.apollo3.api.http.HttpRequest
15-
import com.apollographql.apollo3.api.http.HttpResponse
16-
import com.apollographql.apollo3.api.http.valueOf
17-
import com.apollographql.apollo3.cache.http.CachingHttpInterceptor.Companion.OPERATION_NAME_HEADER
18-
import com.apollographql.apollo3.interceptor.ApolloInterceptor
19-
import com.apollographql.apollo3.interceptor.ApolloInterceptorChain
10+
import com.apollographql.apollo3.cache.http.internal.CacheHeadersHttpInterceptor
11+
import com.apollographql.apollo3.cache.http.internal.HttpCacheApolloInterceptor
2012
import com.apollographql.apollo3.network.http.HttpInfo
21-
import com.apollographql.apollo3.network.http.HttpInterceptor
22-
import com.apollographql.apollo3.network.http.HttpInterceptorChain
2313
import com.apollographql.apollo3.network.http.HttpNetworkTransport
24-
import kotlinx.coroutines.flow.Flow
25-
import kotlinx.coroutines.flow.onCompletion
26-
import kotlinx.coroutines.flow.onEach
2714
import okio.FileSystem
2815
import java.io.File
29-
import java.io.IOException
3016

3117
enum class HttpFetchPolicy {
3218
/**
@@ -80,81 +66,26 @@ fun ApolloClient.Builder.httpCache(
8066
apolloHttpCache: ApolloHttpCache,
8167
): ApolloClient.Builder {
8268
val cachingHttpInterceptor = CachingHttpInterceptor(apolloHttpCache)
83-
8469
val apolloRequestToCacheKey = mutableMapOf<String, String>()
85-
return addHttpInterceptor(object : HttpInterceptor {
86-
override suspend fun intercept(request: HttpRequest, chain: HttpInterceptorChain): HttpResponse {
87-
val cacheKey = CachingHttpInterceptor.cacheKey(request)
88-
val requestUuid = request.headers.valueOf(CachingHttpInterceptor.REQUEST_UUID_HEADER)!!
89-
synchronized(apolloRequestToCacheKey) {
90-
apolloRequestToCacheKey[requestUuid] = cacheKey
91-
}
92-
return chain.proceed(
93-
request.newBuilder()
94-
.headers(request.headers.filterNot { it.name == CachingHttpInterceptor.REQUEST_UUID_HEADER })
95-
.addHeader(CachingHttpInterceptor.CACHE_KEY_HEADER, cacheKey)
96-
.build()
97-
)
70+
return apply {
71+
httpInterceptors.firstOrNull { it is CacheHeadersHttpInterceptor }?.let {
72+
removeHttpInterceptor(it)
9873
}
99-
}).addHttpInterceptor(
100-
cachingHttpInterceptor
101-
).addInterceptor(object : ApolloInterceptor {
102-
override fun <D : Operation.Data> intercept(request: ApolloRequest<D>, chain: ApolloInterceptorChain): Flow<ApolloResponse<D>> {
103-
val policy = getPolicy(request)
104-
val policyStr = when (policy) {
105-
HttpFetchPolicy.CacheFirst -> CachingHttpInterceptor.CACHE_FIRST
106-
HttpFetchPolicy.CacheOnly -> CachingHttpInterceptor.CACHE_ONLY
107-
HttpFetchPolicy.NetworkFirst -> CachingHttpInterceptor.NETWORK_FIRST
108-
HttpFetchPolicy.NetworkOnly -> CachingHttpInterceptor.NETWORK_ONLY
109-
}
110-
111-
return chain.proceed(
112-
request.newBuilder()
113-
.addHttpHeader(
114-
CachingHttpInterceptor.CACHE_OPERATION_TYPE_HEADER,
115-
when (request.operation) {
116-
is Query<*> -> "query"
117-
is Mutation<*> -> "mutation"
118-
is Subscription<*> -> "subscription"
119-
else -> error("Unknown operation type")
120-
}
121-
)
122-
.addHttpHeader(CachingHttpInterceptor.CACHE_FETCH_POLICY_HEADER, policyStr)
123-
.addHttpHeader(CachingHttpInterceptor.REQUEST_UUID_HEADER, request.requestUuid.toString())
124-
.addHttpHeader(OPERATION_NAME_HEADER, request.operation.name())
125-
.build()
126-
)
127-
.run {
128-
if (request.operation is Query<*>) {
129-
onEach { response ->
130-
// Revert caching of responses with errors
131-
val cacheKey = synchronized(apolloRequestToCacheKey) { apolloRequestToCacheKey[request.requestUuid.toString()] }
132-
if (response.hasErrors() || response.exception != null) {
133-
try {
134-
cacheKey?.let { cachingHttpInterceptor.cache.remove(it) }
135-
} catch (_: IOException) {
136-
}
137-
}
138-
}.onCompletion {
139-
synchronized(apolloRequestToCacheKey) { apolloRequestToCacheKey.remove(request.requestUuid.toString()) }
140-
}
141-
} else {
142-
this
143-
}
144-
}
74+
httpInterceptors.firstOrNull { it is CachingHttpInterceptor }?.let {
75+
removeHttpInterceptor(it)
14576
}
146-
})
147-
}
148-
149-
private fun getPolicy(request: ApolloRequest<*>): HttpFetchPolicy {
150-
return if (request.operation is Mutation<*>) {
151-
// Don't cache mutations
152-
HttpFetchPolicy.NetworkOnly
153-
} else {
154-
request.executionContext[HttpFetchPolicyContext]?.httpFetchPolicy ?: HttpFetchPolicy.CacheFirst
15577
}
78+
.addHttpInterceptor(CacheHeadersHttpInterceptor(apolloRequestToCacheKey))
79+
.addHttpInterceptor(cachingHttpInterceptor)
80+
.apply {
81+
interceptors.firstOrNull { it is HttpCacheApolloInterceptor }?.let {
82+
removeInterceptor(it)
83+
}
84+
}
85+
.addInterceptor(HttpCacheApolloInterceptor(apolloRequestToCacheKey, cachingHttpInterceptor))
15686
}
15787

88+
15889
val <D : Operation.Data> ApolloResponse<D>.isFromHttpCache
15990
get() = executionContext[HttpInfo]?.headers?.any {
16091
// This will return true whatever the value in the header. We might want to fine tune this
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.apollographql.apollo3.cache.http.internal
2+
3+
import com.apollographql.apollo3.api.http.HttpRequest
4+
import com.apollographql.apollo3.api.http.HttpResponse
5+
import com.apollographql.apollo3.api.http.valueOf
6+
import com.apollographql.apollo3.cache.http.CachingHttpInterceptor
7+
import com.apollographql.apollo3.network.http.HttpInterceptor
8+
import com.apollographql.apollo3.network.http.HttpInterceptorChain
9+
10+
internal class CacheHeadersHttpInterceptor(private val apolloRequestToCacheKey: MutableMap<String, String>) : HttpInterceptor {
11+
override suspend fun intercept(request: HttpRequest, chain: HttpInterceptorChain): HttpResponse {
12+
val cacheKey = CachingHttpInterceptor.cacheKey(request)
13+
val requestUuid = request.headers.valueOf(CachingHttpInterceptor.REQUEST_UUID_HEADER)!!
14+
synchronized(apolloRequestToCacheKey) {
15+
apolloRequestToCacheKey[requestUuid] = cacheKey
16+
}
17+
return chain.proceed(
18+
request.newBuilder()
19+
.headers(request.headers.filterNot { it.name == CachingHttpInterceptor.REQUEST_UUID_HEADER })
20+
.addHeader(CachingHttpInterceptor.CACHE_KEY_HEADER, cacheKey)
21+
.build()
22+
)
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package com.apollographql.apollo3.cache.http.internal
2+
3+
import com.apollographql.apollo3.api.ApolloRequest
4+
import com.apollographql.apollo3.api.ApolloResponse
5+
import com.apollographql.apollo3.api.Mutation
6+
import com.apollographql.apollo3.api.Operation
7+
import com.apollographql.apollo3.api.Query
8+
import com.apollographql.apollo3.api.Subscription
9+
import com.apollographql.apollo3.cache.http.CachingHttpInterceptor
10+
import com.apollographql.apollo3.cache.http.HttpFetchPolicy
11+
import com.apollographql.apollo3.cache.http.HttpFetchPolicyContext
12+
import com.apollographql.apollo3.interceptor.ApolloInterceptor
13+
import com.apollographql.apollo3.interceptor.ApolloInterceptorChain
14+
import kotlinx.coroutines.flow.Flow
15+
import kotlinx.coroutines.flow.onCompletion
16+
import kotlinx.coroutines.flow.onEach
17+
import java.io.IOException
18+
19+
internal class HttpCacheApolloInterceptor(
20+
private val apolloRequestToCacheKey: MutableMap<String, String>,
21+
private val cachingHttpInterceptor: CachingHttpInterceptor,
22+
) : ApolloInterceptor {
23+
override fun <D : Operation.Data> intercept(request: ApolloRequest<D>, chain: ApolloInterceptorChain): Flow<ApolloResponse<D>> {
24+
val policy = getPolicy(request)
25+
val policyStr = when (policy) {
26+
HttpFetchPolicy.CacheFirst -> CachingHttpInterceptor.CACHE_FIRST
27+
HttpFetchPolicy.CacheOnly -> CachingHttpInterceptor.CACHE_ONLY
28+
HttpFetchPolicy.NetworkFirst -> CachingHttpInterceptor.NETWORK_FIRST
29+
HttpFetchPolicy.NetworkOnly -> CachingHttpInterceptor.NETWORK_ONLY
30+
}
31+
32+
return chain.proceed(
33+
request.newBuilder()
34+
.addHttpHeader(
35+
CachingHttpInterceptor.CACHE_OPERATION_TYPE_HEADER,
36+
when (request.operation) {
37+
is Query<*> -> "query"
38+
is Mutation<*> -> "mutation"
39+
is Subscription<*> -> "subscription"
40+
else -> error("Unknown operation type")
41+
}
42+
)
43+
.addHttpHeader(CachingHttpInterceptor.CACHE_FETCH_POLICY_HEADER, policyStr)
44+
.addHttpHeader(CachingHttpInterceptor.REQUEST_UUID_HEADER, request.requestUuid.toString())
45+
.addHttpHeader(CachingHttpInterceptor.OPERATION_NAME_HEADER, request.operation.name())
46+
.build()
47+
)
48+
.run {
49+
if (request.operation is Query<*>) {
50+
onEach { response ->
51+
// Revert caching of responses with errors
52+
val cacheKey = synchronized(apolloRequestToCacheKey) { apolloRequestToCacheKey[request.requestUuid.toString()] }
53+
if (response.hasErrors() || response.exception != null) {
54+
try {
55+
cacheKey?.let { cachingHttpInterceptor.cache.remove(it) }
56+
} catch (_: IOException) {
57+
}
58+
}
59+
}.onCompletion {
60+
synchronized(apolloRequestToCacheKey) { apolloRequestToCacheKey.remove(request.requestUuid.toString()) }
61+
}
62+
} else {
63+
this
64+
}
65+
}
66+
}
67+
68+
private fun getPolicy(request: ApolloRequest<*>): HttpFetchPolicy {
69+
return if (request.operation is Mutation<*>) {
70+
// Don't cache mutations
71+
HttpFetchPolicy.NetworkOnly
72+
} else {
73+
request.executionContext[HttpFetchPolicyContext]?.httpFetchPolicy ?: HttpFetchPolicy.CacheFirst
74+
}
75+
}
76+
}

libraries/apollo-runtime/api/android/apollo-runtime.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ public final class com/apollographql/apollo3/ApolloClient$Builder : com/apollogr
118118
public final fun httpServerUrl (Ljava/lang/String;)Lcom/apollographql/apollo3/ApolloClient$Builder;
119119
public final fun interceptors (Ljava/util/List;)Lcom/apollographql/apollo3/ApolloClient$Builder;
120120
public final fun networkTransport (Lcom/apollographql/apollo3/network/NetworkTransport;)Lcom/apollographql/apollo3/ApolloClient$Builder;
121+
public final fun removeHttpInterceptor (Lcom/apollographql/apollo3/network/http/HttpInterceptor;)Lcom/apollographql/apollo3/ApolloClient$Builder;
121122
public final fun removeInterceptor (Lcom/apollographql/apollo3/interceptor/ApolloInterceptor;)Lcom/apollographql/apollo3/ApolloClient$Builder;
122123
public fun sendApqExtensions (Ljava/lang/Boolean;)Lcom/apollographql/apollo3/ApolloClient$Builder;
123124
public synthetic fun sendApqExtensions (Ljava/lang/Boolean;)Ljava/lang/Object;

libraries/apollo-runtime/api/jvm/apollo-runtime.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ public final class com/apollographql/apollo3/ApolloClient$Builder : com/apollogr
118118
public final fun httpServerUrl (Ljava/lang/String;)Lcom/apollographql/apollo3/ApolloClient$Builder;
119119
public final fun interceptors (Ljava/util/List;)Lcom/apollographql/apollo3/ApolloClient$Builder;
120120
public final fun networkTransport (Lcom/apollographql/apollo3/network/NetworkTransport;)Lcom/apollographql/apollo3/ApolloClient$Builder;
121+
public final fun removeHttpInterceptor (Lcom/apollographql/apollo3/network/http/HttpInterceptor;)Lcom/apollographql/apollo3/ApolloClient$Builder;
121122
public final fun removeInterceptor (Lcom/apollographql/apollo3/interceptor/ApolloInterceptor;)Lcom/apollographql/apollo3/ApolloClient$Builder;
122123
public fun sendApqExtensions (Ljava/lang/Boolean;)Lcom/apollographql/apollo3/ApolloClient$Builder;
123124
public synthetic fun sendApqExtensions (Ljava/lang/Boolean;)Ljava/lang/Object;

libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/ApolloClient.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,13 @@ private constructor(
616616
_httpInterceptors += httpInterceptor
617617
}
618618

619+
/**
620+
* Removes [httpInterceptor] from the list of HTTP interceptors.
621+
*/
622+
fun removeHttpInterceptor(httpInterceptor: HttpInterceptor) = apply {
623+
_httpInterceptors -= httpInterceptor
624+
}
625+
619626
/**
620627
* The url of the GraphQL server used for WebSockets
621628
* Use this function or webSocketServerUrl((suspend () -> String)) but not both.
@@ -856,6 +863,7 @@ private constructor(
856863
httpMethodForDocumentQueries: HttpMethod = HttpMethod.Post,
857864
enableByDefault: Boolean = true,
858865
) = apply {
866+
_interceptors.removeAll { it is AutoPersistedQueryInterceptor }
859867
addInterceptor(
860868
AutoPersistedQueryInterceptor(
861869
httpMethodForHashedQueries,
@@ -883,6 +891,7 @@ private constructor(
883891
maxBatchSize: Int = 10,
884892
enableByDefault: Boolean = true,
885893
) = apply {
894+
_httpInterceptors.removeAll { it is BatchingHttpInterceptor }
886895
addHttpInterceptor(BatchingHttpInterceptor(batchIntervalMillis, maxBatchSize))
887896
canBeBatched(enableByDefault)
888897
}

tests/http-cache/src/test/kotlin/HttpCacheTest.kt

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
21
import com.apollographql.apollo3.ApolloClient
2+
import com.apollographql.apollo3.api.http.HttpResponse
3+
import com.apollographql.apollo3.cache.http.ApolloHttpCache
4+
import com.apollographql.apollo3.cache.http.DiskLruHttpCache
35
import com.apollographql.apollo3.cache.http.HttpFetchPolicy
46
import com.apollographql.apollo3.cache.http.httpCache
57
import com.apollographql.apollo3.cache.http.httpExpireTimeout
@@ -22,6 +24,7 @@ import kotlinx.coroutines.delay
2224
import kotlinx.coroutines.runBlocking
2325
import okhttp3.Interceptor
2426
import okhttp3.OkHttpClient
27+
import okio.FileSystem
2528
import java.io.File
2629
import kotlin.test.Test
2730
import kotlin.test.assertEquals
@@ -200,7 +203,8 @@ class HttpCacheTest {
200203
"setRandom": "42"
201204
}
202205
}
203-
""".trimIndent())
206+
""".trimIndent()
207+
)
204208
apolloClient.mutation(mutation)
205209
.httpFetchPolicy(HttpFetchPolicy.CacheOnly)
206210
.execute()
@@ -231,7 +235,8 @@ class HttpCacheTest {
231235
},
232236
"errors": [ { "message": "GraphQL error" } ]
233237
}
234-
""")
238+
"""
239+
)
235240
apolloClient.query(GetRandomQuery()).execute()
236241
// Should not have been cached
237242
assertIs<HttpCacheMissException>(
@@ -275,4 +280,46 @@ class HttpCacheTest {
275280
}
276281
}
277282

283+
@Test
284+
fun httpCacheCleansPreviousInterceptor() = runTest {
285+
mockServer = MockServer()
286+
val httpCache1 = CountingApolloHttpCache()
287+
mockServer.enqueueData(data)
288+
val apolloClient = ApolloClient.Builder()
289+
.serverUrl(mockServer.url())
290+
.httpCache(httpCache1)
291+
.build()
292+
apolloClient.query(GetRandomQuery()).execute()
293+
assertEquals(1, httpCache1.writes)
294+
295+
val httpCache2 = CountingApolloHttpCache()
296+
val apolloClient2 = apolloClient.newBuilder()
297+
.httpCache(httpCache2)
298+
.build()
299+
mockServer.enqueueData(data)
300+
apolloClient2.query(GetRandomQuery()).execute()
301+
assertEquals(1, httpCache1.writes)
302+
assertEquals(1, httpCache2.writes)
303+
}
304+
}
305+
306+
private class CountingApolloHttpCache : ApolloHttpCache {
307+
private val wrapped = run {
308+
val dir = File("build/httpCache")
309+
dir.deleteRecursively()
310+
DiskLruHttpCache(FileSystem.SYSTEM, dir, Long.MAX_VALUE)
311+
}
312+
var writes = 0
313+
override fun write(response: HttpResponse, cacheKey: String): HttpResponse {
314+
writes++
315+
return wrapped.write(response, cacheKey)
316+
}
317+
318+
override fun read(cacheKey: String): HttpResponse {
319+
return wrapped.read(cacheKey)
320+
}
321+
322+
override fun clearAll() {}
323+
324+
override fun remove(cacheKey: String) {}
278325
}

0 commit comments

Comments
 (0)