Skip to content

Commit 1e12753

Browse files
martinbonninBoD
andauthored
Add HTTP caching using OkHttp cacheUrlOverride (#6739)
* Add HTTP caching using OkHttp cacheUrlOverride * fix integration tests * Add more comments * Move the logic to HttpRequestComposer * reenable incremental compilation * Disable K/N incremental compilation * update apiDump * update documentation and deprecation * Make our AI overloads happy * fix variable name * Update docs/source/migration/5.0.mdx Co-authored-by: Benoit 'BoD' Lubek <BoD@JRAF.org> --------- Co-authored-by: Benoit 'BoD' Lubek <BoD@JRAF.org>
1 parent c0bc87b commit 1e12753

File tree

45 files changed

+581
-610
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+581
-610
lines changed

build-logic/src/main/kotlin/api.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class AndroidOptions(
3333
)
3434

3535
class KotlinCompilerOptions(
36-
val version: KotlinVersion = KotlinVersion.KOTLIN_2_0,
36+
val version: KotlinVersion = KotlinVersion.KOTLIN_2_1,
3737
)
3838

3939
fun Project.version(): String {

docs/source/caching/http-cache.mdx

Lines changed: 42 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -7,97 +7,70 @@ title: HTTP cache
77
88
<Note>
99

10-
The HTTP cache is only available on Android and the JVM.
10+
HTTP caching is only available on the JVM.
1111

1212
</Note>
1313

14-
## Setup
15-
16-
To enable HTTP cache support, add the dependency to your project's `build.gradle` file:
17-
18-
```kotlin title="build.gradle[.kts]"
19-
dependencies {
20-
implementation("com.apollographql.apollo:apollo-http-cache:5.0.0-alpha.2")
21-
}
22-
```
23-
24-
If you're targeting Android API level < 26, you'll need to enable [core library desugaring](https://developer.android.com/studio/write/java8-support#library-desugaring) to support the `java.time` API:
14+
Apollo uses OkHttp's [cacheUrlOverride](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-request/-builder/cache-url-override.html) to cache GraphQL POST requests when [persisted queries](https://www.apollographql.com/docs/kotlin/advanced/persisted-queries) and GET requests are not an option.
2515

26-
```kotlin
27-
android {
28-
compileOptions {
29-
// Flag to enable support for the new language APIs
30-
coreLibraryDesugaringEnabled = true
31-
}
32-
}
33-
```
16+
## Setup
3417

35-
Then configure your HTTP cache:
18+
Configure your `NetworkTransport` with your cache-enabled `OkHttpClient` and `cachePostRequests` set to `true`:
3619

3720
```kotlin
38-
apolloClient = ApolloClient.Builder()
39-
.httpCache(
40-
// Use a dedicated directory for the cache
41-
directory = File(pathToCacheDirectory),
42-
// Configure a max size of 100MB
43-
maxSize = 100 * 1024 * 1024
21+
val apolloClient = ApolloClient.Builder()
22+
.networkTransport(
23+
HttpNetworkTransport.Builder()
24+
// Enable POST caching
25+
.httpRequestComposer(DefaultHttpRequestComposer(serverUrl = serverUrl, enablePostCaching = true))
26+
.httpEngine(
27+
DefaultHttpEngine {
28+
OkHttpClient.Builder()
29+
.cache(directory = File(application.cacheDir, "http_cache"), maxSize = 10_000_000)
30+
.build()
31+
}
32+
)
33+
.build()
4434
)
4535
.build()
4636
```
4737

48-
## Usage
38+
Under the hood, the OkHttp `HttpEngine` uses `"${url}?variablesHash=${variables.sha256()}&operationName=${operation.name()}&operationId=${operation.id()}"` as a <code>cacheUrlOverride</code> to cache POST requests.
4939

50-
The HTTP cache is a Least Recently Used (LRU) cache with a configurable max size.
40+
## Usage
5141

52-
Once your cache setup is complete, the cache will be used by default by all your queries. By default, queries will try to find a result in the cache first and go the network if it's not there. This is the `HttpFetchPolicy.CacheFirst` policy. You can customize that behaviour with `httpFetchPolicy(HttpFetchPolicy)`:
42+
OkHttp uses the [cache-control header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control):
5343

5444
```kotlin
55-
val response = apolloClient.query(query)
56-
// Try the cache first - if it's a miss, try the network
57-
.httpFetchPolicy(HttpFetchPolicy.CacheFirst)
58-
59-
// Only use the cache
60-
.httpFetchPolicy(HttpFetchPolicy.CacheOnly)
61-
62-
// Try the network first - if there's an error, try the cache
63-
.httpFetchPolicy(HttpFetchPolicy.NetworkFirst)
64-
65-
// Don't use the cache
66-
.httpFetchPolicy(HttpFetchPolicy.NetworkOnly)
67-
68-
// Finally, execute your query
69-
.execute()
45+
val successResponse = MockResponse.Builder()
46+
.body(FooQuery.successResponse)
47+
.addHeader("cache-control", "max-age=100")
48+
.build()
49+
mockServer.enqueue(successResponse)
50+
51+
// first query: goes to the network and caches the response
52+
val response1 = apolloClient.query(FooQuery()).execute()
53+
println(response1.isFromHttpCache) // false
54+
// second query: uses the cached response
55+
val response2 = apolloClient.query(FooQuery()).execute()
56+
println(response2.isFromHttpCache) // true
7057
```
71-
Note: mutations and subscriptions don't go through the cache.
7258

73-
If the query is present in cache, it will be used to return `response.data`. If not, a `HttpCacheMissException` will be thrown.
74-
75-
You can also set an expiration time either globally or for specific queries. The entries will automatically be removed from the cache after the expiration time:
59+
You can use `ApolloResponse.isFromHttpCache` to determine if a given request comes from the cache:
7660

7761
```kotlin
78-
// Globally
79-
apolloClient = ApolloClient.Builder()
80-
.httpCache(/*...*/)
81-
// Expire after 1 hour
82-
.httpExpireTimeout(60 * 60 * 1000)
83-
.build()
84-
85-
// On a specific query
86-
val response = apolloClient.query(query)
87-
// Expire after 1 hour
88-
.httpExpireTimeout(60 * 60 * 1000)
89-
.execute()
62+
val response = apolloClient.query(FooQuery()).execute()
63+
assertEquals(false, response.isFromHttpCache)
9064
```
9165

92-
If a specific query must not be cached, you can use `httpDoNotStore()`:
66+
To disable caching for a particular request, use standard HTTP headers. For example, you can use `no-store` to disable storing a response:
9367

9468
```kotlin
95-
val response = apolloClient.query(query)
96-
// Don't cache this query
97-
.httpDoNotStore(httpDoNotStore = true)
69+
val response1 = apolloClient.query(FooQuery())
70+
.addHttpHeader("cache-control", "no-store") // this response is not stored
71+
.execute()
72+
assertEquals(false, response1.isFromHttpCache)
73+
val response2 = apolloClient.query(FooQuery()) // a new HTTP request is issued.
9874
.execute()
75+
assertEquals(false, response2.isFromHttpCache)
9976
```
100-
101-
## Clearing the cache
102-
103-
Call `apolloClient.httpCache.clearAll()` to clear the cache of all entries.

docs/source/index.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,8 @@ Some platforms have specific runtime requirements:
150150
At build time, it requires:
151151

152152
* Gradle 8.0+
153-
* Kotlin 2.0+ for JVM projects
154-
* Kotlin 2.1+ for native, JS, and Wasm projects
153+
* Kotlin 2.1+ for JVM projects
154+
* Kotlin 2.2+ for native, JS, and Wasm projects
155155

156156
## Proguard / R8 configuration
157157

docs/source/migration/5.0.mdx

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,83 @@ query GetUser {
9696
```
9797

9898
You can read more in the ["handling nullability" page](https://www.apollographql.com/docs/kotlin/advanced/nullability).
99+
100+
101+
## `apollo-http-cache` is deprecated
102+
103+
Apollo Kotlin 5 removes the `apollo-http-cache` artifact. Instead, it uses the existing OkHttp cache using [cacheUrlOverride](https://square.github.io/okhttp/5.x/okhttp/okhttp3/-request/-builder/cache-url-override.html).
104+
105+
You can remove the `apollo-http-cache` artifact:
106+
107+
```kotlin
108+
dependencies {
109+
// Remove
110+
implementation("com.apollographql.apollo:apollo-http-cache:4.3.3")
111+
}
112+
```
113+
114+
Configure your `ApolloClient` with a cache-enabled `OkHttpClient` and set `enablePostCaching` to true:
115+
116+
```kotlin
117+
// Replace
118+
val apolloClient = ApolloClient.Builder()
119+
.serverUrl(mockServer.url())
120+
.httpCache(directory = "http_cache", maxSize = 10_000_000)
121+
.build()
122+
123+
// With
124+
val apolloClient = ApolloClient.Builder()
125+
.networkTransport(
126+
HttpNetworkTransport.Builder()
127+
// Enable POST caching
128+
.httpRequestComposer(DefaultHttpRequestComposer(serverUrl = mockServer.url(), enablePostCaching = true))
129+
.httpEngine(
130+
DefaultHttpEngine {
131+
OkHttpClient.Builder()
132+
// Make sure to use a different directory for your cache
133+
.cache(directory = "http_cache2", maxSize = 10_000_000)
134+
.build()
135+
}
136+
)
137+
.build()
138+
)
139+
.build()
140+
```
141+
142+
The OkHttp cache uses the standard `Cache-Control` HTTP header. Compared to Apollo Kotlin 4 and `HttpFetchPolicy` the semantics are different but you can map most of the concepts:
143+
144+
**CacheFirst**:
145+
```kotlin
146+
// Replace
147+
apolloClient.query(query).httpFetchPolicy(HttpFetchPolicy.CacheFirst)
148+
149+
// With
150+
apolloClient.query(query) // no need to add a cache-control header, this is the default
151+
```
152+
153+
**CacheOnly**:
154+
```kotlin
155+
// Replace
156+
apolloClient.query(query).httpFetchPolicy(HttpFetchPolicy.CacheOnly)
157+
158+
// With
159+
apolloClient.query(query).addHttpHeader("cache-control", "only-if-cached")
160+
```
161+
162+
**NetworkOnly**:
163+
```kotlin
164+
// Replace
165+
apolloClient.query(query).httpFetchPolicy(HttpFetchPolicy.CacheOnly)
166+
167+
// With
168+
apolloClient.query(query).addHttpHeader("cache-control", "no-cache")
169+
```
170+
171+
### Limitations
172+
173+
We believe this new caching scheme is simpler and more aligned with the rest of the ecosystem, but there are important differences with the previous scheme:
174+
175+
- There is no equivalent to `NetworkFirst` and/or `httpExpireTimeout()`.
176+
- The cache keys have changed, meaning your cache will be invalidated.
177+
178+
If either of these limitations is important for your use case, please [open an issue](https://github.com/apollographql/apollo-kotlin/issues/new).

gradle.properties

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ kotlin.internal.collectFUSMetrics=false
2424
# Remove when https://github.com/google/ksp/pull/2554 is released
2525
ksp.version.check=false
2626

27-
kotlin.incremental.native=true
27+
# See https://youtrack.jetbrains.com/issue/KT-81708
28+
kotlin.incremental.native=false

gradle/libraries.toml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,8 @@ ksp = "2.2.20-2.0.4"
3939
#noinspection NewerVersionAvailable requires KGP 2.2
4040
ktor = "3.2.3"
4141
moshix = "0.14.1"
42-
#noinspection NewerVersionAvailable requires KGP 2.2
43-
okio = "3.15.0"
44-
#noinspection NewerVersionAvailable requires KGP 2.2
45-
okhttp = "4.12.0"
42+
okio = "3.16.1"
43+
okhttp = "5.2.1"
4644
sqldelight = "2.1.0"
4745
truth = "1.4.5"
4846

@@ -157,6 +155,7 @@ okhttp-mockwebserver = { group = "com.squareup.okhttp3", name = "mockwebserver",
157155
okhttp-tls = { group = "com.squareup.okhttp3", name = "okhttp-tls", version.ref = "okhttp" }
158156
moshi = { group = "com.squareup.moshi", name = "moshi", version = "1.15.2" }
159157
okio = { group = "com.squareup.okio", name = "okio", version.ref = "okio" }
158+
okio-fakefilesystem = { group = "com.squareup.okio", name = "okio-fakefilesystem", version.ref = "okio" }
160159
okio-nodefilesystem = { group = "com.squareup.okio", name = "okio-nodefilesystem", version.ref = "okio" }
161160
poet-java = { group = "com.squareup", name = "javapoet", version.ref = "javaPoet" }
162161
# Depend on the -jvm artifact directly to workaround https://github.com/gradle/gradle/issues/31158
Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,13 @@
1+
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
2+
13
plugins {
24
id("org.jetbrains.kotlin.multiplatform")
35
}
46

57
apolloLibrary(
68
namespace = "com.apollographql.apollo.annotations",
7-
description = "Apollo GraphQL Annotations"
9+
description = "Apollo GraphQL Annotations",
10+
kotlinCompilerOptions = KotlinCompilerOptions(KotlinVersion.KOTLIN_2_0), // For better Gradle compatibility
811
)
912

10-
kotlin {
11-
sourceSets {
12-
/**
13-
* Because apollo-annotation is pulled as an API dependency in in all other modules we configure the
14-
* Kotlin stdlib dependency here. See https://youtrack.jetbrains.com/issue/KT-53471
15-
*/
16-
findByName("commonMain")?.apply {
17-
dependencies {
18-
api(libs.kotlin.stdlib.common)
19-
}
20-
}
21-
findByName("jvmMain")?.apply {
22-
dependencies {
23-
api(libs.kotlin.stdlib.jvm)
24-
}
25-
}
26-
findByName("jsMain")?.apply {
27-
dependencies {
28-
api(libs.kotlin.stdlib.js)
29-
}
30-
}
31-
findByName("wasmJsMain")!!.apply {
32-
dependencies {
33-
api(libs.kotlin.stdlib.wasm.js)
34-
}
35-
}
36-
}
37-
}
13+

libraries/apollo-api/api/apollo-api.api

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -925,9 +925,20 @@ public final class com/apollographql/apollo/api/http/ByteStringHttpBody : com/ap
925925
public fun writeTo (Lokio/BufferedSink;)V
926926
}
927927

928+
public final class com/apollographql/apollo/api/http/CacheUrlOverride : com/apollographql/apollo/api/ExecutionContext$Element {
929+
public static final field Key Lcom/apollographql/apollo/api/http/CacheUrlOverride$Key;
930+
public fun <init> (Ljava/lang/String;)V
931+
public fun getKey ()Lcom/apollographql/apollo/api/ExecutionContext$Key;
932+
public final fun getUrl ()Ljava/lang/String;
933+
}
934+
935+
public final class com/apollographql/apollo/api/http/CacheUrlOverride$Key : com/apollographql/apollo/api/ExecutionContext$Key {
936+
}
937+
928938
public final class com/apollographql/apollo/api/http/DefaultHttpRequestComposer : com/apollographql/apollo/api/http/HttpRequestComposer {
929939
public static final field Companion Lcom/apollographql/apollo/api/http/DefaultHttpRequestComposer$Companion;
930940
public fun <init> (Ljava/lang/String;)V
941+
public fun <init> (Ljava/lang/String;Z)V
931942
public fun compose (Lcom/apollographql/apollo/api/ApolloRequest;)Lcom/apollographql/apollo/api/http/HttpRequest;
932943
}
933944

@@ -1011,15 +1022,17 @@ public abstract interface class com/apollographql/apollo/api/http/HttpRequestCom
10111022
}
10121023

10131024
public final class com/apollographql/apollo/api/http/HttpResponse {
1014-
public synthetic fun <init> (ILjava/util/List;Lokio/BufferedSource;Lokio/ByteString;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
1025+
public synthetic fun <init> (ILjava/util/List;Lokio/BufferedSource;Lokio/ByteString;Lcom/apollographql/apollo/api/ExecutionContext;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
10151026
public final fun getBody ()Lokio/BufferedSource;
1027+
public final fun getExecutionContext ()Lcom/apollographql/apollo/api/ExecutionContext;
10161028
public final fun getHeaders ()Ljava/util/List;
10171029
public final fun getStatusCode ()I
10181030
public final fun newBuilder ()Lcom/apollographql/apollo/api/http/HttpResponse$Builder;
10191031
}
10201032

10211033
public final class com/apollographql/apollo/api/http/HttpResponse$Builder {
10221034
public fun <init> (I)V
1035+
public final fun addExecutionContext (Lcom/apollographql/apollo/api/ExecutionContext;)Lcom/apollographql/apollo/api/http/HttpResponse$Builder;
10231036
public final fun addHeader (Ljava/lang/String;Ljava/lang/String;)Lcom/apollographql/apollo/api/http/HttpResponse$Builder;
10241037
public final fun addHeaders (Ljava/util/List;)Lcom/apollographql/apollo/api/http/HttpResponse$Builder;
10251038
public final fun body (Lokio/BufferedSource;)Lcom/apollographql/apollo/api/http/HttpResponse$Builder;

libraries/apollo-api/api/apollo-api.klib.api

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,8 +491,20 @@ final class com.apollographql.apollo.api.http/ByteStringHttpBody : com.apollogra
491491
final fun writeTo(okio/BufferedSink) // com.apollographql.apollo.api.http/ByteStringHttpBody.writeTo|writeTo(okio.BufferedSink){}[0]
492492
}
493493

494+
final class com.apollographql.apollo.api.http/CacheUrlOverride : com.apollographql.apollo.api/ExecutionContext.Element { // com.apollographql.apollo.api.http/CacheUrlOverride|null[0]
495+
constructor <init>(kotlin/String) // com.apollographql.apollo.api.http/CacheUrlOverride.<init>|<init>(kotlin.String){}[0]
496+
497+
final val key // com.apollographql.apollo.api.http/CacheUrlOverride.key|{}key[0]
498+
final fun <get-key>(): com.apollographql.apollo.api/ExecutionContext.Key<*> // com.apollographql.apollo.api.http/CacheUrlOverride.key.<get-key>|<get-key>(){}[0]
499+
final val url // com.apollographql.apollo.api.http/CacheUrlOverride.url|{}url[0]
500+
final fun <get-url>(): kotlin/String // com.apollographql.apollo.api.http/CacheUrlOverride.url.<get-url>|<get-url>(){}[0]
501+
502+
final object Key : com.apollographql.apollo.api/ExecutionContext.Key<com.apollographql.apollo.api.http/CacheUrlOverride> // com.apollographql.apollo.api.http/CacheUrlOverride.Key|null[0]
503+
}
504+
494505
final class com.apollographql.apollo.api.http/DefaultHttpRequestComposer : com.apollographql.apollo.api.http/HttpRequestComposer { // com.apollographql.apollo.api.http/DefaultHttpRequestComposer|null[0]
495506
constructor <init>(kotlin/String) // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.<init>|<init>(kotlin.String){}[0]
507+
constructor <init>(kotlin/String, kotlin/Boolean) // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.<init>|<init>(kotlin.String;kotlin.Boolean){}[0]
496508

497509
final fun <#A1: com.apollographql.apollo.api/Operation.Data> compose(com.apollographql.apollo.api/ApolloRequest<#A1>): com.apollographql.apollo.api.http/HttpRequest // com.apollographql.apollo.api.http/DefaultHttpRequestComposer.compose|compose(com.apollographql.apollo.api.ApolloRequest<0:0>){0§<com.apollographql.apollo.api.Operation.Data>}[0]
498510

@@ -564,6 +576,8 @@ final class com.apollographql.apollo.api.http/HttpRequest { // com.apollographql
564576
final class com.apollographql.apollo.api.http/HttpResponse { // com.apollographql.apollo.api.http/HttpResponse|null[0]
565577
final val body // com.apollographql.apollo.api.http/HttpResponse.body|{}body[0]
566578
final fun <get-body>(): okio/BufferedSource? // com.apollographql.apollo.api.http/HttpResponse.body.<get-body>|<get-body>(){}[0]
579+
final val executionContext // com.apollographql.apollo.api.http/HttpResponse.executionContext|{}executionContext[0]
580+
final fun <get-executionContext>(): com.apollographql.apollo.api/ExecutionContext // com.apollographql.apollo.api.http/HttpResponse.executionContext.<get-executionContext>|<get-executionContext>(){}[0]
567581
final val headers // com.apollographql.apollo.api.http/HttpResponse.headers|{}headers[0]
568582
final fun <get-headers>(): kotlin.collections/List<com.apollographql.apollo.api.http/HttpHeader> // com.apollographql.apollo.api.http/HttpResponse.headers.<get-headers>|<get-headers>(){}[0]
569583
final val statusCode // com.apollographql.apollo.api.http/HttpResponse.statusCode|{}statusCode[0]
@@ -577,6 +591,7 @@ final class com.apollographql.apollo.api.http/HttpResponse { // com.apollographq
577591
final val statusCode // com.apollographql.apollo.api.http/HttpResponse.Builder.statusCode|{}statusCode[0]
578592
final fun <get-statusCode>(): kotlin/Int // com.apollographql.apollo.api.http/HttpResponse.Builder.statusCode.<get-statusCode>|<get-statusCode>(){}[0]
579593

594+
final fun addExecutionContext(com.apollographql.apollo.api/ExecutionContext): com.apollographql.apollo.api.http/HttpResponse.Builder // com.apollographql.apollo.api.http/HttpResponse.Builder.addExecutionContext|addExecutionContext(com.apollographql.apollo.api.ExecutionContext){}[0]
580595
final fun addHeader(kotlin/String, kotlin/String): com.apollographql.apollo.api.http/HttpResponse.Builder // com.apollographql.apollo.api.http/HttpResponse.Builder.addHeader|addHeader(kotlin.String;kotlin.String){}[0]
581596
final fun addHeaders(kotlin.collections/List<com.apollographql.apollo.api.http/HttpHeader>): com.apollographql.apollo.api.http/HttpResponse.Builder // com.apollographql.apollo.api.http/HttpResponse.Builder.addHeaders|addHeaders(kotlin.collections.List<com.apollographql.apollo.api.http.HttpHeader>){}[0]
582597
final fun body(okio/BufferedSource): com.apollographql.apollo.api.http/HttpResponse.Builder // com.apollographql.apollo.api.http/HttpResponse.Builder.body|body(okio.BufferedSource){}[0]

0 commit comments

Comments
 (0)