Repository: apollographql/apollo-kotlin
Pull request #6170 (2024-10-01 09:51:49)

Add support for application/graphql-response+json
The new content-type allows us to have more guarantees when a response is a valid GraphQL response vs potential error from an intermediate node. 

See https://graphql.github.io/graphql-over-http/draft/#sec-application-graphql-response-json
See https://github.com/apollographql/apollo-kotlin/issues/6169

netlify[bot]: ### <span aria-hidden="true">👷</span> Deploy Preview for *apollo-android-docs* processing.


|  Name | Link |
|:-:|------------------------|
|<span aria-hidden="true">🔨</span> Latest commit | 56e2162ececa32161d57bfe1ecee4e741410529f |
|<span aria-hidden="true">🔍</span> Latest deploy log | https://app.netlify.com/sites/apollo-android-docs/deploys/66fbc6387eb3eb00084535d6 |

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3984883f4e..8d3a34de7f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,79 @@
 Change Log
 ==========
 
-# Version v4.0.0
+# Version 4.0.1
+
+_2024-10-01_
+
+This release contains a handful of bug fixes and improvements. 
+
+## ⚙️ Add `ApolloCompilerPlugin.schemaListener()`
+
+The [compiler plugins](https://www.apollographql.com/docs/kotlin/advanced/compiler-plugins) API has been extended to allow listening to schema changes.\
+This can be used to have plugins generate code based on the schema. To do this, implement the [`schemaListener`](https://www.apollographql.com/docs/kotlin/kdoc/apollo-compiler/com.apollographql.apollo.compiler/-apollo-compiler-plugin/schema-listener.html)
+function on your plugin:
+    
+```kotlin
+class MyCompilerPlugin() : ApolloCompilerPlugin {
+  @ApolloExperimental
+  override fun schemaListener(): SchemaListener {
+    return object : SchemaListener {
+      override fun onSchema(schema: Schema, outputDirectory: File) {
+        // Generate some code inside outputDirectory based on schema
+      }
+    }
+  }
+}
+```
+
+## 🚀 Allow mapping scalars to simple generic types
+
+Previously, to [map a scalar](https://www.apollographql.com/docs/kotlin/essentials/custom-scalars) to a generic type, you had to use a `typealias`. 
+Now, simple generic types are accepted, and common types like `List` and `String` don't need to be fully qualified:
+
+```kotlin
+mapScalar("MassList", "List<com.example.Measure<com.example.Mass>>", "com.example.MassListAdapter")
+```
+
+## 💙 Contributors
+
+Many thanks to @ebrattli, @agrosner and @cvb941 for their contributions to this release 💙!
+
+## 👷 All changes
+
+* [gradle-plugin] Deprecate `"operationOutput"` and `./gradlew downloadApolloSchema` (#6097)
+* [gradle-plugin] Use `registerJavaGeneratingTask` (#6149)
+* [apollo-ast] Add GQLDocument.validate(SchemaValidationOptions) to allow adding external schemas. (#6164)
+* [compiler] Add ApolloCompilerPlugin.schemaDocumentListener() (#6165)
+* [compiler] Pass schema to ApolloCompilerPlugin.schemaListener (#6166)
+* [compiler] Avoid enum value clashing with the getter `field` (#6093)
+* [compiler] Fix a few additional instances of %L used instead of %N (#6117)
+* [compiler] Escape properties in input builder function body (#6116)
+* [compiler] Provide a more descriptive error message when a resolution of a specific `ResolverKey` fails (#6136)
+* [compiler] Deprecate `@nonnull` (#6152)
+* [compiler] Allow mapping scalars to simple generic types (#6158)
+* [tooling] Allow to shutdown SchemaDownloader (#6091)
+* [tooling] Remove warning (#6092)
+* [WebSockets] connectionParams -> connectionPayload (#6103)
+* [WebSockets] add NetworkTransport.closeConnection() (#6105)
+* [BREAKING][runtime] Change NetworkMonitor to expose a StateFlow directly (#6119)
+* [runtime] Add 'OfflineException' as a cause when using failFastIfOffline (#6104)
+* [apollo-api] Fix reading `JsonNumber` in `MapJsonReader` (#6141)
+* [cache] Allow to store `JsonNumber` in `Record` (#6139)
+* [cache] Fix cascade SQL cache remove loops forever on cyclic references (#6137)
+* [IJ Plugin] Bump pluginUntilBuild to 242 and pluginSinceBuild to 241 (#6111)
+* [IJ Plugin] Add ApolloOneOfGraphQLViolationInspection (#6125)
+* [IJ Plugin] Improve performance of ApolloGraphQLConfigFilePresentInspection and ApolloGraphQLConfigFilePresentAnnotator (#6126)
+* [IJ Plugin] Fix high latency field inspection (#6142)
+* [IJ Plugin] Correctly name Open In items (#6151)
+* [infra] Bump Kotlin to 2.0.10 (#6107)
+* [infra] Bump Kotlin to 2.0.20 (#6131)
+* [infra] Bump develocity (#6128)
+* [infra] Update Apollo Execution (#6138)
+* [infra] Bump develocity (#6144)
+* [infra] Allow compiling the project with Java22 (#6145)
+
+# Version 4.0.0
 
 _2024-07-29_
 
@@ -197,7 +269,7 @@ Thanks again to everyone who provided feedback during the alphas/betas.
 * [#650](https://github.com/apollographql/apollo-kotlin/pull/650) - Create Android Studio Plugin
 
 
-# Version v4.0.0-rc.2
+# Version 4.0.0-rc.2
 
 _2024-07-22_
 
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 14e5ab4a66..7c116654fd 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -208,7 +208,7 @@ Here are the steps to do a new release:
 
 * `git checkout main && git pull`
 * `scripts/release.main.kts <version-name>`
-* while it compiles, prepare the changelog, open a PR to `CHANGELOG.md`
+* while it compiles, prepare the changelog, open a PR to `CHANGELOG.md` (see below)
 * wait for the CI to finish compiling
 * go to https://s01.oss.sonatype.org/, and release the artifacts manually. This step is called "close, release and drop"
   in the Sonatype ecosystem.
@@ -220,6 +220,13 @@ Here are the steps to do a new release:
 * if it's a significant release, tweet about it 🐦
 * relax 🍹
 
+### Changelog file
+* Add a section with the version, date, and a quick summary of what the release contains.
+* Optionally add a few sections to zoom in on changes you want to highlight.
+* No need to highlight deprecations, as warnings in the code are enough.
+* Mention and thank external contributors if any.
+* Add an "All changes" section that should list all commits since last release (can use `git log --pretty=oneline <previous-tag>..main`). Commits on the documentation can be omitted.
+
 ## Debugging minimized Gradle Plugin stacktraces
 
 Because the Gradle plugin uses [R8](https://r8.googlesource.com/r8) to relocate dependencies, the stacktraces do not
diff --git a/docs/source/advanced/apollo-ast.mdx b/docs/source/advanced/apollo-ast.mdx
index 7fd7fb632c..cf339f477a 100644
--- a/docs/source/advanced/apollo-ast.mdx
+++ b/docs/source/advanced/apollo-ast.mdx
@@ -21,7 +21,7 @@ Add the `apollo-ast` dependency to your project:
 dependencies {
   // ...
 
-  implementation("com.apollographql.apollo:apollo-ast:4.0.0")
+  implementation("com.apollographql.apollo:apollo-ast:4.0.1")
 }
 ```
 
diff --git a/docs/source/advanced/compiler-plugins.mdx b/docs/source/advanced/compiler-plugins.mdx
index 10e74c5bee..f033e58823 100644
--- a/docs/source/advanced/compiler-plugins.mdx
+++ b/docs/source/advanced/compiler-plugins.mdx
@@ -33,7 +33,7 @@ plugins {
 
 dependencies {
   // Add apollo-compiler as a dependency
-  implementation("com.apollographql.apollo:apollo-compiler:4.0.0")  
+  implementation("com.apollographql.apollo:apollo-compiler:4.0.1")  
 }
 ```
 
diff --git a/docs/source/advanced/experimental-websockets.mdx b/docs/source/advanced/experimental-websockets.mdx
index 7df564bf55..38b6c2c0a7 100644
--- a/docs/source/advanced/experimental-websockets.mdx
+++ b/docs/source/advanced/experimental-websockets.mdx
@@ -34,6 +34,8 @@ After a feedback phase, the current `.ws` APIs will become deprecated and the `.
 
 ## Migration guide
 
+### Package name
+
 In simple cases where you did not configure the underlying `WsProtocol` or retry logic, the migration should be about replacing `com.apollographql.apollo.network.ws` with `com.apollographql.apollo.network.websocket` everywhere:
 
 ```kotlin
@@ -80,35 +82,60 @@ ApolloClient.Builder()
     .build()
 ```
 
-To account for non-websocket transports, like [multipart subscriptions](https://www.apollographql.com/docs/router/executing-operations/subscription-multipart-protocol/), the retry is now handled on the `ApolloClient` instead of the `NetworkTransport`.
+### Connection init payload
+
+If you were using `connectionPayload` before, you can now pass it as an argument directly. There is no `WsProtocol.Factory` anymore:
+
+```kotlin
+// Replace
+GraphQLWsProtocol.Factory(
+    connectionPayload = {
+        mapOf("Authorization" to token)
+    },
+)
+
+// With
+GraphQLWsProtocol(
+    connectionPayload = {
+      mapOf("Authorization" to token)
+    },
+)
+```
+
+### Retrying on network errors
+
+Apollo Kotlin 4 also comes with a default `retryOnErrorInterceptor` that uses a network monitor or exponential backoff to retry the subscription. 
+
+If you want your subscription to be restarted automatically when a network error happens, use `retryOnError {}`:
 
 ```kotlin
 // Replace
 val apolloClient = ApolloClient.Builder()
+    .serverUrl(serverUrl)
     .subscriptionNetworkTransport(
         WebSocketNetworkTransport.Builder()
-            .serverUrl(url)
-            .reopenWhen { _, _ ->
-              delay(1000)
+            .serverUrl(webSocketServerUrl)
+            .reopenWhen { _, attempt ->
+              // exponential backoff
+              delay(2.0.pow(attempt).seconds) // highlight-line
               true
             }
             .build()
     )
+    .build()
 
-// With
+// With 
 val apolloClient = ApolloClient.Builder()
-    .subscriptionNetworkTransport(
-        WebSocketNetworkTransport.Builder()
-            .serverUrl(url)
-            .build()
-    )
-    // Only retry subscriptions
-    .retryOnError { it.operation is Subscription }
+    .serverUrl(serverUrl)
+    .subscriptionNetworkTransport(/*..*/)
+    .retryOnError {
+      /*
+       * This is called for every GraphQL operation.
+       * Only retry subscriptions.
+       */
+      it.operation is Subscription // highlight-line
+    }
+    .build()
 ```
 
-The above uses the default retry algorithm:
-
-* Wait until the network is available if you configured a [NetworkMonitor](network-connectivity).
-* Or use exponential backoff else.
-
-To customize the retry logic more, see the [network monitoring page](../advanced/network-connectivity#customizing-the-retry-algorithm).
\ No newline at end of file
+You can also customize the retry logic using `retryOnErrorInterceptor`. Read more about it in the [network connectivity page](network-connectivity). 
diff --git a/docs/source/advanced/no-runtime.mdx b/docs/source/advanced/no-runtime.mdx
index b290047908..5bfd4a5ae0 100644
--- a/docs/source/advanced/no-runtime.mdx
+++ b/docs/source/advanced/no-runtime.mdx
@@ -7,7 +7,7 @@ title: Using the models without apollo-runtime
 For this, remove the `com.apollographql.apollo:apollo-runtime` dependency and replace it with:
 
 ```kotlin title="build.gradle"
-implementation("com.apollographql.apollo:apollo-api:4.0.0")
+implementation("com.apollographql.apollo:apollo-api:4.0.1")
 ```
 
 ## Composing an HTTP request body
diff --git a/docs/source/caching/http-cache.mdx b/docs/source/caching/http-cache.mdx
index c54a44b780..54951901a7 100644
--- a/docs/source/caching/http-cache.mdx
+++ b/docs/source/caching/http-cache.mdx
@@ -17,7 +17,7 @@ To enable HTTP cache support, add the dependency to your project's `build.gradle
 
 ```kotlin title="build.gradle[.kts]"
 dependencies {
-  implementation("com.apollographql.apollo:apollo-http-cache:4.0.0")
+  implementation("com.apollographql.apollo:apollo-http-cache:4.0.1")
 }
 ```
 
diff --git a/docs/source/caching/normalized-cache.mdx b/docs/source/caching/normalized-cache.mdx
index f926aa5ae0..2832fc8780 100644
--- a/docs/source/caching/normalized-cache.mdx
+++ b/docs/source/caching/normalized-cache.mdx
@@ -74,7 +74,7 @@ Apollo Kotlin's `MemoryCache` is a normalized, in-memory cache for storing objec
 
 ```kotlin title="build.gradle[.kts]"
 dependencies {
-  implementation("com.apollographql.apollo:apollo-normalized-cache:4.0.0")
+  implementation("com.apollographql.apollo:apollo-normalized-cache:4.0.1")
 }
 ```
 
@@ -110,7 +110,7 @@ To enable SQLite cache support, add the `apollo-normalized-cache-sqlite` depende
 
 ```kotlin title="build.gradle.kts"
 dependencies {
-  implementation("com.apollographql.apollo:apollo-normalized-cache-sqlite:4.0.0")
+  implementation("com.apollographql.apollo:apollo-normalized-cache-sqlite:4.0.1")
 }
 ```
 
diff --git a/docs/source/index.mdx b/docs/source/index.mdx
index d37d7fed38..7447bae32d 100644
--- a/docs/source/index.mdx
+++ b/docs/source/index.mdx
@@ -39,7 +39,7 @@ Add the plugin to your `build.gradle.kts`:
 
 ```kotlin
 plugins {
-  id("com.apollographql.apollo") version "4.0.0"
+  id("com.apollographql.apollo") version "4.0.1"
 }
 ```
 
@@ -47,7 +47,7 @@ Add the runtime dependency:
 
 ```kotlin
 dependencies {
-  implementation("com.apollographql.apollo:apollo-runtime:4.0.0")
+  implementation("com.apollographql.apollo:apollo-runtime:4.0.1")
 }
 ```
 
@@ -164,7 +164,7 @@ Installation instructions and more information can be found [here](testing/andro
 
 ## Releases
 
-The latest version is `4.0.0`.
+The latest version is `4.0.1`.
 
 Check the [changelog](https://github.com/apollographql/apollo-kotlin/releases) for the release history.
 
@@ -172,7 +172,7 @@ Releases are hosted on [Maven Central](https://repo1.maven.org/maven2/com/apollo
 
 ```kotlin
 plugins {
-  id("com.apollographql.apollo") version "4.0.0"
+  id("com.apollographql.apollo") version "4.0.1"
 }
 
 repositories {
@@ -180,13 +180,13 @@ repositories {
 }
 
 dependencies {
-  implementation("com.apollographql.apollo:apollo-runtime:4.0.0")
+  implementation("com.apollographql.apollo:apollo-runtime:4.0.1")
 
   // Optional: if you want to use the normalized cache
-  implementation("com.apollographql.apollo:apollo-normalized-cache-sqlite:4.0.0")
+  implementation("com.apollographql.apollo:apollo-normalized-cache-sqlite:4.0.1")
   // Optional: if you just want the generated models and parsers and write your own HTTP code/cache code, you can remove apollo-runtime
   // and use apollo-api instead
-  implementation("com.apollographql.apollo:apollo-api:4.0.0")
+  implementation("com.apollographql.apollo:apollo-api:4.0.1")
 }
 ```
 
@@ -217,7 +217,7 @@ pluginManagement {
 }
 ```
 
-And then use the `4.0.1-SNAPSHOT` version for the plugin and libraries.
+And then use the `4.0.2-SNAPSHOT` version for the plugin and libraries.
 
 These snapshots are updated on each push to `main`.
 
diff --git a/docs/source/tutorial/01-configure-project.mdx b/docs/source/tutorial/01-configure-project.mdx
index bd372e9442..57edfb3c02 100644
--- a/docs/source/tutorial/01-configure-project.mdx
+++ b/docs/source/tutorial/01-configure-project.mdx
@@ -43,7 +43,7 @@ Go to `Android Studio` -> `Setting...` -> `Plugins`. In the "Marketplace" tab, s
 
 You can find the latest version of Apollo Kotlin from the [GitHub releases page](https://github.com/apollographql/apollo-kotlin/releases). It is also displayed at the top of the [apollo-kotlin repo](https://github.com/apollographql/apollo-kotlin/).
 
-This tutorial uses `4.0.0` because it is the latest version at the time of writing. Feel free to use a more recent version if one is available.
+This tutorial uses `4.0.1` because it is the latest version at the time of writing. Feel free to use a more recent version if one is available.
 
 ## Apply the plugin
 
diff --git a/gradle.properties b/gradle.properties
index cec7bbc0d2..315325cede 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,6 +1,6 @@
 # Publishing defaults, could ultimately be moved to build scripts
 GROUP=com.apollographql.apollo
-VERSION_NAME=4.0.1-SNAPSHOT
+VERSION_NAME=4.0.2-SNAPSHOT
 
 POM_URL=https://github.com/apollographql/apollo-kotlin/
 POM_SCM_URL=https://github.com/apollographql/apollo-kotlin/
diff --git a/gradle/libraries.toml b/gradle/libraries.toml
index 14dbd673e8..8de1a6621d 100644
--- a/gradle/libraries.toml
+++ b/gradle/libraries.toml
@@ -14,7 +14,7 @@ android-sdkversion-benchmark-min = "24"
 android-sdkversion-target = "30"
 androidx-sqlite = "2.3.1"
 # This is used by the gradle integration tests to get the artifacts locally
-apollo = "4.0.1-SNAPSHOT"
+apollo = "4.0.2-SNAPSHOT"
 apollo-execution = "0.0.3"
 apollo-normalizedcache-incubating-snapshot = "0.0.3-SNAPSHOT"
 # Used by the apollo-tooling project which uses a published version of Apollo
diff --git a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/refactoring/migration/v3tov4/ApolloV3ToV4MigrationProcessor.kt b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/refactoring/migration/v3tov4/ApolloV3ToV4MigrationProcessor.kt
index 197befbdd8..5903abd17e 100644
--- a/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/refactoring/migration/v3tov4/ApolloV3ToV4MigrationProcessor.kt
+++ b/intellij-plugin/src/main/kotlin/com/apollographql/ijplugin/refactoring/migration/v3tov4/ApolloV3ToV4MigrationProcessor.kt
@@ -34,7 +34,7 @@ import com.intellij.openapi.project.Project
  */
 class ApolloV3ToV4MigrationProcessor(project: Project) : ApolloMigrationRefactoringProcessor(project) {
   companion object {
-    const val apollo4LatestVersion = "4.0.0"
+    const val apollo4LatestVersion = "4.0.1"
   }
 
   override val refactoringName = ApolloBundle.message("ApolloV3ToV4MigrationProcessor.title")
diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/http/DefaultHttpRequestComposer.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/http/DefaultHttpRequestComposer.kt
index 6012349431..d17bbf1515 100644
--- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/http/DefaultHttpRequestComposer.kt
+++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo/api/http/DefaultHttpRequestComposer.kt
@@ -106,8 +106,8 @@ class DefaultHttpRequestComposer(
     // TODO The deferSpec=20220824 part is a temporary measure so early backend implementations of the @defer directive
     // can recognize early client implementations and potentially reply in a compatible way.
     // This should be removed in later versions.
-    val HEADER_ACCEPT_VALUE_DEFER = "multipart/mixed;deferSpec=20220824, application/json"
-    val HEADER_ACCEPT_VALUE_MULTIPART = "multipart/mixed;subscriptionSpec=1.0, application/json"
+    val HEADER_ACCEPT_VALUE_DEFER = "multipart/mixed;deferSpec=20220824, application/graphql-response+json, application/json"
+    val HEADER_ACCEPT_VALUE_MULTIPART = "multipart/mixed;subscriptionSpec=1.0, application/graphql-response+json, application/json"
 
     private fun <D : Operation.Data> buildGetUrl(
         serverUrl: String,
diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/multipart.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/multipart.kt
index 113f956e96..b5d7d2cba5 100644
--- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/multipart.kt
+++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/internal/multipart.kt
@@ -47,3 +47,6 @@ private fun getBoundaryParameter(contentType: String?): String? {
 
 internal val HttpResponse.isMultipart: Boolean
   get() = headers.valueOf("Content-Type")?.startsWith("multipart/", ignoreCase = true) == true
+
+internal val HttpResponse.isGraphQLResponse: Boolean
+  get() = headers.valueOf("Content-Type")?.startsWith("application/graphql-response+json", ignoreCase = true) == true
diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt
index 169fd3e42c..159d349c46 100644
--- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt
+++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo/network/http/HttpNetworkTransport.kt
@@ -22,6 +22,7 @@ import com.apollographql.apollo.exception.ApolloHttpException
 import com.apollographql.apollo.exception.ApolloNetworkException
 import com.apollographql.apollo.exception.RouterError
 import com.apollographql.apollo.internal.DeferredJsonMerger
+import com.apollographql.apollo.internal.isGraphQLResponse
 import com.apollographql.apollo.internal.isMultipart
 import com.apollographql.apollo.internal.multipartBodyFlow
 import com.apollographql.apollo.mpp.currentTimeMillis
@@ -72,12 +73,19 @@ private constructor(
         null
       }
 
-      emitAll(when {
+      val responses = when {
         httpResponse == null -> {
           flowOf(errorResponse(request.operation, apolloException!!))
         }
 
-        httpResponse.statusCode !in 200..299 -> {
+        httpResponse.statusCode !in 200..299 && !httpResponse.isGraphQLResponse -> {
+          /*
+           * application/json may contain something else than a GraphQL response.
+           * Typically, this happens if a proxy/other non-GraphQL intermediary encounters an error.
+           *
+           * In those cases, don't try to parse the body
+           * See https://graphql.github.io/graphql-over-http/draft/#sec-Processing-the-response
+           */
           errorResponse(request.operation, httpResponse)
         }
 
@@ -91,10 +99,10 @@ private constructor(
           singleResponse(request.operation, customScalarAdapters, httpResponse)
         }
       }
-          .map {
-            it.withHttpInfo(request.requestUuid, httpResponse, millisStart)
-          }
-      )
+
+      emitAll(responses.map {
+        it.withHttpInfo(request.requestUuid, httpResponse, millisStart)
+      })
     }
   }
 
@@ -164,7 +172,7 @@ private constructor(
             var errors: List<Error>? = null
             reader.beginObject()
             while (reader.hasNext()) {
-              when(reader.nextName()) {
+              when (reader.nextName()) {
                 "payload" -> {
                   if (reader.peek() == JsonReader.Token.NULL) {
                     reader.skipValue()
@@ -176,6 +184,7 @@ private constructor(
                     )
                   }
                 }
+
                 "errors" -> {
                   if (reader.peek() == JsonReader.Token.NULL) {
                     reader.skipValue()
@@ -183,6 +192,7 @@ private constructor(
                     errors = reader.readErrors()
                   }
                 }
+
                 else -> {
                   // Ignore unknown keys
                   reader.skipValue()
@@ -194,6 +204,7 @@ private constructor(
               errors != null -> {
                 errorResponse(operation, RouterError(errors))
               }
+
               payloadResponse != null -> payloadResponse
               else -> null
             }


Test patch:
diff --git a/docs/source/testing/apollo-debug-server.mdx b/docs/source/testing/apollo-debug-server.mdx
index 9f2873adc6..e8b12dfd9b 100644
--- a/docs/source/testing/apollo-debug-server.mdx
+++ b/docs/source/testing/apollo-debug-server.mdx
@@ -17,7 +17,7 @@ dependencies {
   // ...
 
   // For security, add the dependency to your debug builds only
-  debugImplementation("com.apollographql.apollo:apollo-debug-server:4.0.0")
+  debugImplementation("com.apollographql.apollo:apollo-debug-server:4.0.1")
 }
 ```
 
diff --git a/docs/source/testing/mocking-graphql-responses.mdx b/docs/source/testing/mocking-graphql-responses.mdx
index c473e97d87..3e5b975a73 100644
--- a/docs/source/testing/mocking-graphql-responses.mdx
+++ b/docs/source/testing/mocking-graphql-responses.mdx
@@ -14,7 +14,7 @@ Add the dependency to your project's `build.gradle` file:
 
 ```kotlin title="build.gradle[.kts]"
 dependencies {
-  testImplementation("com.apollographql.apollo:apollo-testing-support:4.0.0")
+  testImplementation("com.apollographql.apollo:apollo-testing-support:4.0.1")
 }
 ```
 
diff --git a/intellij-plugin/src/test/testData/migration/v3-to-v4/updateGradleDependenciesInLibsVersionsToml_after.versions.toml b/intellij-plugin/src/test/testData/migration/v3-to-v4/updateGradleDependenciesInLibsVersionsToml_after.versions.toml
index ef128f5e2c..702c3bb7b3 100644
--- a/intellij-plugin/src/test/testData/migration/v3-to-v4/updateGradleDependenciesInLibsVersionsToml_after.versions.toml
+++ b/intellij-plugin/src/test/testData/migration/v3-to-v4/updateGradleDependenciesInLibsVersionsToml_after.versions.toml
@@ -1,19 +1,19 @@
 [versions]
 # gradlePlugin versions
 androidBuildTools = "7.2.1"
-apollo = "4.0.0"
+apollo = "4.0.1"
 
 [libraries]
 accompanist-insets = { module = "com.google.accompanist:accompanist-insets", version.ref = "accompanist" }
-apollo-runtime-short = "com.apollographql.apollo:apollo-runtime:4.0.0"
-apollo-runtime-medium-version = { module = "com.apollographql.apollo:apollo-runtime", version = "4.0.0" }
+apollo-runtime-short = "com.apollographql.apollo:apollo-runtime:4.0.1"
+apollo-runtime-medium-version = { module = "com.apollographql.apollo:apollo-runtime", version = "4.0.1" }
 apollo-runtime-medium-ref = { module = "com.apollographql.apollo:apollo-runtime", version.ref = "apollo" }
-apollo-runtime-long-version = { group = "com.apollographql.apollo", name = "apollo-runtime", version = "4.0.0" }
+apollo-runtime-long-version = { group = "com.apollographql.apollo", name = "apollo-runtime", version = "4.0.1" }
 apollo-runtime-long-ref = { group = "com.apollographql.apollo", name = "apollo-runtime", version.ref = "apollo" }
 
 [plugins]
-apollo-shortNotation = "com.apollographql.apollo:4.0.0"
+apollo-shortNotation = "com.apollographql.apollo:4.0.1"
 apollo-longNotation = { id = "com.apollographql.apollo", version.ref = "apollo" }
-apollo-referenceNotation = { id = "com.apollographql.apollo", version = "4.0.0" }
+apollo-referenceNotation = { id = "com.apollographql.apollo", version = "4.0.1" }
 apollo-shortNotationNoVersion = "com.apollographql.apollo"
 apollo-longNotationNoVersion = { id = "com.apollographql.apollo" }
diff --git a/intellij-plugin/src/test/testData/migration/v3-to-v4/upgradeGradlePluginInBuildGradleKts_after.gradle.kts b/intellij-plugin/src/test/testData/migration/v3-to-v4/upgradeGradlePluginInBuildGradleKts_after.gradle.kts
index ad407f34c2..25988997f5 100644
--- a/intellij-plugin/src/test/testData/migration/v3-to-v4/upgradeGradlePluginInBuildGradleKts_after.gradle.kts
+++ b/intellij-plugin/src/test/testData/migration/v3-to-v4/upgradeGradlePluginInBuildGradleKts_after.gradle.kts
@@ -2,16 +2,16 @@ plugins {
   java
   kotlin("jvm") version "1.6.10"
   id("com.apollographql.apollo")
-  id("com.apollographql.apollo") version "4.0.0"
-  id("com.apollographql.apollo") version "4.0.0" apply false
-  // TODO: Update version to 4.0.0
+  id("com.apollographql.apollo") version "4.0.1"
+  id("com.apollographql.apollo") version "4.0.1" apply false
+  // TODO: Update version to 4.0.1
   id("com.apollographql.apollo") version someClass.someConstant
-  // TODO: Update version to 4.0.0
+  // TODO: Update version to 4.0.1
   id("com.apollographql.apollo") version "${someClass.someConstant}"
-  id("com.apollographql.apollo") version "4.0.0"
-  id("com.apollographql.apollo") version "4.0.0" apply false
-  // TODO: Update version to 4.0.0
+  id("com.apollographql.apollo") version "4.0.1"
+  id("com.apollographql.apollo") version "4.0.1" apply false
+  // TODO: Update version to 4.0.1
   id("com.apollographql.apollo") version someClass.someConstant
-  // TODO: Update version to 4.0.0
+  // TODO: Update version to 4.0.1
   id("com.apollographql.apollo") version "${someClass.someConstant}"
 }
diff --git a/libraries/apollo-runtime/src/commonTest/kotlin/test/FooOperation.kt b/libraries/apollo-runtime/src/commonTest/kotlin/test/FooOperation.kt
index 60ad50c11e..c02be6ad68 100644
--- a/libraries/apollo-runtime/src/commonTest/kotlin/test/FooOperation.kt
+++ b/libraries/apollo-runtime/src/commonTest/kotlin/test/FooOperation.kt
@@ -24,6 +24,7 @@ import com.apollographql.apollo.api.missingField
 internal class FooQuery: FooOperation("query"), Query<FooOperation.Data> {
   companion object {
     val successResponse = "{\"data\": {\"foo\": 42}}"
+    val errorResponse = "{\"errors\": [{\"message\": \"Oh no! Something went wrong :(\"}]}"
   }
 }
 
diff --git a/libraries/apollo-runtime/src/commonTest/kotlin/test/network/HttpNetworkTransportTest.kt b/libraries/apollo-runtime/src/commonTest/kotlin/test/network/HttpNetworkTransportTest.kt
new file mode 100644
index 0000000000..f72af97a0d
--- /dev/null
+++ b/libraries/apollo-runtime/src/commonTest/kotlin/test/network/HttpNetworkTransportTest.kt
@@ -0,0 +1,26 @@
+package test.network
+
+import com.apollographql.apollo.ApolloClient
+import com.apollographql.apollo.testing.internal.runTest
+import com.apollographql.mockserver.MockServer
+import com.apollographql.mockserver.enqueueString
+import okio.use
+import test.FooQuery
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class HttpNetworkTransportTest {
+  @Test
+  fun graphqlResponseJsonContentTypeMayHaveNon2xxHttpCode() = runTest {
+    MockServer().use { mockServer ->
+      ApolloClient.Builder()
+          .serverUrl(mockServer.url())
+          .build()
+          .use { apolloClient ->
+            mockServer.enqueueString(string = FooQuery.errorResponse, statusCode = 500, contentType = "application/graphql-response+json")
+            val response = apolloClient.query(FooQuery()).execute()
+            assertEquals("Oh no! Something went wrong :(", response.errors?.single()?.message)
+          }
+    }
+  }
+}
\ No newline at end of file
diff --git a/libraries/apollo-runtime/src/jvmTest/kotlin/RetryWebSocketsTest.kt b/libraries/apollo-runtime/src/jvmTest/kotlin/RetryWebSocketsTest.kt
index 3a02753d0d..114bc106a7 100644
--- a/libraries/apollo-runtime/src/jvmTest/kotlin/RetryWebSocketsTest.kt
+++ b/libraries/apollo-runtime/src/jvmTest/kotlin/RetryWebSocketsTest.kt
@@ -1,19 +1,29 @@
-
 import app.cash.turbine.test
 import com.apollographql.apollo.ApolloClient
+import com.apollographql.apollo.api.ApolloRequest
+import com.apollographql.apollo.api.ApolloResponse
+import com.apollographql.apollo.api.Operation
 import com.apollographql.apollo.api.Subscription
 import com.apollographql.apollo.exception.ApolloHttpException
 import com.apollographql.apollo.exception.ApolloNetworkException
+import com.apollographql.apollo.interceptor.ApolloInterceptor
+import com.apollographql.apollo.interceptor.ApolloInterceptorChain
 import com.apollographql.apollo.network.websocket.WebSocketNetworkTransport
 import com.apollographql.apollo.testing.connectionAckMessage
 import com.apollographql.apollo.testing.internal.runTest
 import com.apollographql.mockserver.MockResponse
 import com.apollographql.mockserver.MockServer
+import com.apollographql.mockserver.assertNoRequest
 import com.apollographql.mockserver.awaitWebSocketRequest
 import com.apollographql.mockserver.enqueueWebSocket
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.catch
 import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.retryWhen
 import kotlinx.coroutines.flow.take
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withTimeout
@@ -28,6 +38,7 @@ import test.network.mockServerTest
 import test.network.retryWhen
 import kotlin.test.Test
 import kotlin.test.assertEquals
+import kotlin.test.assertFails
 import kotlin.test.assertIs
 import kotlin.test.assertNotEquals
 import kotlin.time.Duration.Companion.seconds
@@ -37,56 +48,56 @@ class RetryWebSocketsTest {
   fun retryIsWorking() = runTest {
     MockServer().use { mockServer ->
 
-    ApolloClient.Builder()
-        .serverUrl(mockServer.url())
-        .retryOnError { it.operation is Subscription }
-        .subscriptionNetworkTransport(
-            WebSocketNetworkTransport.Builder()
-                .serverUrl(mockServer.url())
-                .build()
-        )
-        .build().use { apolloClient ->
-          apolloClient.subscription(FooSubscription())
-              .toFlow()
-              .test {
-                val serverWriter = mockServer.enqueueWebSocket(keepAlive = false)
-                var serverReader = mockServer.awaitWebSocketRequest()
+      ApolloClient.Builder()
+          .serverUrl(mockServer.url())
+          .retryOnError { it.operation is Subscription }
+          .subscriptionNetworkTransport(
+              WebSocketNetworkTransport.Builder()
+                  .serverUrl(mockServer.url())
+                  .build()
+          )
+          .build().use { apolloClient ->
+            apolloClient.subscription(FooSubscription())
+                .toFlow()
+                .test {
+                  val serverWriter = mockServer.enqueueWebSocket(keepAlive = false)
+                  var serverReader = mockServer.awaitWebSocketRequest()
 
-                serverReader.awaitMessage() // connection_init
-                serverWriter.enqueueMessage(connectionAckMessage())
+                  serverReader.awaitMessage() // connection_init
+                  serverWriter.enqueueMessage(connectionAckMessage())
 
-                val operationId1 = serverReader.awaitSubscribe()
-                serverWriter.enqueueMessage(nextMessage(operationId1, 1))
+                  val operationId1 = serverReader.awaitSubscribe()
+                  serverWriter.enqueueMessage(nextMessage(operationId1, 1))
 
-                val item1 = awaitItem()
-                assertEquals(1, item1.data?.foo)
+                  val item1 = awaitItem()
+                  assertEquals(1, item1.data?.foo)
 
-                val serverWriter2 = mockServer.enqueueWebSocket()
+                  val serverWriter2 = mockServer.enqueueWebSocket()
 
-                /**
-                 * Close the response body and the TCP socket
-                 */
-                serverWriter.close()
+                  /**
+                   * Close the response body and the TCP socket
+                   */
+                  serverWriter.close()
 
-                serverReader = mockServer.awaitWebSocketRequest()
+                  serverReader = mockServer.awaitWebSocketRequest()
 
-                serverReader.awaitMessage() // connection_init
-                serverWriter2.enqueueMessage(connectionAckMessage())
+                  serverReader.awaitMessage() // connection_init
+                  serverWriter2.enqueueMessage(connectionAckMessage())
 
-                val operationId2 = serverReader.awaitSubscribe()
-                serverWriter2.enqueueMessage(nextMessage(operationId2, 2))
+                  val operationId2 = serverReader.awaitSubscribe()
+                  serverWriter2.enqueueMessage(nextMessage(operationId2, 2))
 
-                val item2 = awaitItem()
-                assertEquals(2, item2.data?.foo)
+                  val item2 = awaitItem()
+                  assertEquals(2, item2.data?.foo)
 
-                // The subscriptions MUST use different operationIds
-                assertNotEquals(operationId1, operationId2)
+                  // The subscriptions MUST use different operationIds
+                  assertNotEquals(operationId1, operationId2)
 
-                serverWriter2.enqueueMessage(completeMessage(operationId2))
+                  serverWriter2.enqueueMessage(completeMessage(operationId2))
 
-                awaitComplete()
-              }
-        }
+                  awaitComplete()
+                }
+          }
     }
   }
 
@@ -151,6 +162,71 @@ class RetryWebSocketsTest {
         }
   }
 
+  class MyRetryOnErrorInterceptor : ApolloInterceptor {
+    data object RetryException : Exception()
+
+    override fun <D : Operation.Data> intercept(request: ApolloRequest<D>, chain: ApolloInterceptorChain): Flow<ApolloResponse<D>> {
+      return chain.proceed(request).onEach {
+        if (request.retryOnError == true && it.exception != null && it.exception is ApolloNetworkException) {
+          throw RetryException
+        }
+      }.retryWhen { cause, attempt ->
+        cause is RetryException && attempt < 2
+      }.catch {
+        if (it !is RetryException) throw it
+      }
+    }
+  }
+
+  @Test
+  fun customRetryOnErrorInterceptor() = runTest {
+    val mockServer = MockServer()
+    ApolloClient.Builder()
+        .serverUrl(mockServer.url())
+        .subscriptionNetworkTransport(
+            WebSocketNetworkTransport.Builder()
+                .serverUrl(mockServer.url())
+                .build()
+        )
+        .retryOnError {
+          it.operation is Subscription
+        }
+        .retryOnErrorInterceptor(MyRetryOnErrorInterceptor())
+        .build().use { apolloClient ->
+          var serverWriter = mockServer.enqueueWebSocket(keepAlive = false)
+
+          apolloClient.subscription(FooSubscription())
+              .toFlow()
+              .test {
+                /*
+                 * We retry 2 times, meaning we expect 3 collections
+                 */
+                repeat(3) {
+                  val serverReader = mockServer.awaitWebSocketRequest()
+
+                  serverReader.awaitMessage() // connection_init
+                  serverWriter.enqueueMessage(connectionAckMessage())
+
+                  val operationId = serverReader.awaitSubscribe()
+                  serverWriter.enqueueMessage(nextMessage(operationId, it))
+
+                  val item1 = awaitItem()
+                  assertEquals(it, item1.data?.foo)
+
+                  val lastServerWriter = serverWriter
+                  serverWriter = mockServer.enqueueWebSocket(keepAlive = false)
+                  lastServerWriter.close()
+                }
+                assertFails {
+                  // Make sure that no retry is done
+                  mockServer.awaitAnyRequest(timeout = 1.seconds)
+                }
+                serverWriter.close()
+                awaitComplete()
+              }
+        }
+  }
+
   @Test
   fun retryCanBeDisabled() = runTest {
     val mockServer = MockServer()
diff --git a/tests/integration-tests/src/commonTest/kotlin/test/LoggingInterceptorTest.kt b/tests/integration-tests/src/commonTest/kotlin/test/LoggingInterceptorTest.kt
index 94e70bcb80..4dde38d3c0 100644
--- a/tests/integration-tests/src/commonTest/kotlin/test/LoggingInterceptorTest.kt
+++ b/tests/integration-tests/src/commonTest/kotlin/test/LoggingInterceptorTest.kt
@@ -120,7 +120,7 @@ class LoggingInterceptorTest {
     client.query(HeroNameQuery()).execute()
     logger.assertLog("""
       Post http://0.0.0.0/
-      accept: multipart/mixed;deferspec=20220824, application/json
+      accept: multipart/mixed;deferspec=20220824, application/graphql-response+json, application/json
       [end of headers]
 
       HTTP: 200
@@ -140,7 +140,7 @@ class LoggingInterceptorTest {
     client.query(HeroNameQuery()).execute()
     logger.assertLog("""
       Post http://0.0.0.0/
-      accept: multipart/mixed;deferspec=20220824, application/json
+      accept: multipart/mixed;deferspec=20220824, application/graphql-response+json, application/json
       [end of headers]
       {"operationName":"HeroName","variables":{},"query":"query HeroName { hero { name } }"}
 
@@ -181,7 +181,7 @@ class LoggingInterceptorTest {
     client.query(HeroNameQuery()).execute()
     logger.assertLog("""
       Post http://0.0.0.0/
-      accept: multipart/mixed;deferspec=20220824, application/json
+      accept: multipart/mixed;deferspec=20220824, application/graphql-response+json, application/json
       [end of headers]
       {"operationName":"HeroName","variables":{},"query":"query HeroName { hero { name } }"}