diff --git a/libraries/apollo-api/api/apollo-api.api b/libraries/apollo-api/api/apollo-api.api index be0450862d..4d7d13f1b5 100644 --- a/libraries/apollo-api/api/apollo-api.api +++ b/libraries/apollo-api/api/apollo-api.api @@ -1304,6 +1304,10 @@ public final class com/apollographql/apollo3/exception/NoDataException : com/apo public fun (Ljava/lang/Throwable;)V } +public final class com/apollographql/apollo3/exception/NullOrMissingField : com/apollographql/apollo3/exception/ApolloException { + public fun (Ljava/lang/String;)V +} + public final class com/apollographql/apollo3/exception/RouterError : com/apollographql/apollo3/exception/ApolloException { public fun (Ljava/util/List;)V public final fun getErrors ()Ljava/util/List; diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/Assertions.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/Assertions.kt index ca9f035130..29aaf2ee50 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/Assertions.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/Assertions.kt @@ -4,7 +4,7 @@ package com.apollographql.apollo3.api import com.apollographql.apollo3.api.json.JsonReader -import com.apollographql.apollo3.exception.DefaultApolloException +import com.apollographql.apollo3.exception.NullOrMissingField import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName @@ -13,7 +13,7 @@ import kotlin.jvm.JvmName */ fun checkFieldNotMissing(value: Any?, name: String) { if (value == null) { - throw DefaultApolloException("Field '$name' is missing or null") + throw NullOrMissingField("Field '$name' is missing or null") } } @@ -35,5 +35,5 @@ fun assertOneOf(vararg args: Optional<*>) { * Helper function for the Kotlin codegen */ fun missingField(jsonReader: JsonReader, name: String): Nothing { - throw DefaultApolloException("Field '$name' is missing or null at path ${jsonReader.getPath()}") + throw NullOrMissingField("Field '$name' is missing or null at path ${jsonReader.getPath()}") } diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/Operations.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/Operations.kt index 667a45e768..55057e0f10 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/Operations.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/api/Operations.kt @@ -8,7 +8,7 @@ import com.apollographql.apollo3.api.json.JsonReader import com.apollographql.apollo3.api.json.JsonWriter import com.apollographql.apollo3.api.json.writeObject import com.apollographql.apollo3.exception.ApolloException -import com.apollographql.apollo3.exception.ApolloParseException +import com.apollographql.apollo3.exception.ApolloNetworkException import com.apollographql.apollo3.exception.JsonDataException import com.apollographql.apollo3.exception.JsonEncodingException import com.benasher44.uuid.Uuid @@ -114,10 +114,9 @@ fun Operation.parseResponse( val apolloException = if (throwable is ApolloException) { throwable } else { - // This happens for null pointer exceptions on missing fields - ApolloParseException( - message = "Failed to parse GraphQL http network response", - cause = throwable + ApolloNetworkException( + message = "Error while reading JSON response", + platformCause = throwable ) } return ApolloResponse.Builder(requestUuid = requestUuid ?: uuid4(), operation = this) diff --git a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/exception/Exceptions.kt b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/exception/Exceptions.kt index b56904e3de..43c5a907ef 100644 --- a/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/exception/Exceptions.kt +++ b/libraries/apollo-api/src/commonMain/kotlin/com/apollographql/apollo3/exception/Exceptions.kt @@ -15,7 +15,7 @@ import okio.BufferedSource sealed class ApolloException(message: String? = null, cause: Throwable? = null) : RuntimeException(message, cause) /** - * A generic exception when no additional context exists + * A generic exception used when there is no additional context besides the message. */ class DefaultApolloException(message: String? = null, cause: Throwable? = null): ApolloException(message, cause) @@ -25,7 +25,10 @@ class DefaultApolloException(message: String? = null, cause: Throwable? = null): class NoDataException(cause: Throwable?): ApolloException("No data was found", cause) /** - * A network error happened: socket closed, DNS issue, TLS problem, etc... + * An I/O error happened: socket closed, DNS issue, TLS problem, file not found, etc... + * + * This is called [ApolloNetworkException] for historical reasons, but it should have been `ApolloIOException` instead. + * [ApolloNetworkException] is thrown when an I/O error happens reading the operation. * * @param message a message indicating what the error was. * @param platformCause the underlying cause. Might be null. When not null, it can be cast to: @@ -58,7 +61,6 @@ class SubscriptionConnectionException( val payload: Any?, ) : ApolloException(message = "Subscription connection error") - /** * The router sent one or several errors. * @@ -108,15 +110,26 @@ class JsonEncodingException(message: String) : ApolloException(message) * * Exceptions of this type should be fixed by either changing the application code to accept the unexpected JSON, or by changing the JSON * to conform to the application's expectations. - * - * This exception may also be triggered if a document's nesting exceeds 31 levels. This depth is sufficient for all practical applications, - * but shallow enough to avoid uglier failures like [StackOverflowError]. */ class JsonDataException(message: String) : ApolloException(message) /** - * The response could not be parsed either because of another issue than [JsonDataException] or [JsonEncodingException] + * A field was missing or null in the JSON response. + * + * Due to the way the parsers work, it is not possible to distinguish between both cases. + */ +class NullOrMissingField(message: String): ApolloException(message) + +/** + * The response could not be parsed because of an I/O exception. + * + * JSON and GraphQL errors are throwing other errors, see [JsonEncodingException], [JsonDataException] and [NullOrMissingField] + * + * @see JsonEncodingException + * @see JsonDataException + * @see NullOrMissingField */ +@Deprecated("ApolloParseException was only used for I/O exceptions and is now mapped to ApolloNetworkException.") class ApolloParseException(message: String? = null, cause: Throwable? = null) : ApolloException(message = message, cause = cause) class ApolloGraphQLException(val error: Error): ApolloException("GraphQL error: '${error.message}'") { diff --git a/libraries/apollo-runtime-java/src/main/java/com/apollographql/apollo3/runtime/java/network/http/HttpNetworkTransport.java b/libraries/apollo-runtime-java/src/main/java/com/apollographql/apollo3/runtime/java/network/http/HttpNetworkTransport.java index 2c55797cbc..7ca2b6162b 100644 --- a/libraries/apollo-runtime-java/src/main/java/com/apollographql/apollo3/runtime/java/network/http/HttpNetworkTransport.java +++ b/libraries/apollo-runtime-java/src/main/java/com/apollographql/apollo3/runtime/java/network/http/HttpNetworkTransport.java @@ -69,7 +69,7 @@ public void execute(@NotNull ApolloRequest request } @Override public void onFailure(@NotNull ApolloNetworkException exception) { - callback.onResponse(getExceptionResponse(request, new ApolloParseException("Cannot parse response", exception))); + callback.onResponse(getExceptionResponse(request, exception)); } }); } diff --git a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/http/HttpNetworkTransport.kt b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/http/HttpNetworkTransport.kt index def1995501..02952b257a 100644 --- a/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/http/HttpNetworkTransport.kt +++ b/libraries/apollo-runtime/src/commonMain/kotlin/com/apollographql/apollo3/network/http/HttpNetworkTransport.kt @@ -18,7 +18,7 @@ import com.apollographql.apollo3.api.parseResponse import com.apollographql.apollo3.api.toApolloResponse import com.apollographql.apollo3.exception.ApolloException import com.apollographql.apollo3.exception.ApolloHttpException -import com.apollographql.apollo3.exception.ApolloParseException +import com.apollographql.apollo3.exception.ApolloNetworkException import com.apollographql.apollo3.exception.RouterError import com.apollographql.apollo3.internal.DeferredJsonMerger import com.apollographql.apollo3.internal.isMultipart @@ -104,10 +104,9 @@ private constructor( val apolloException = if (throwable is ApolloException) { throwable } else { - // This happens for null pointer exceptions on missing fields - ApolloParseException( - message = "Failed to parse GraphQL http network response", - cause = throwable + ApolloNetworkException( + message = "Error while reading JSON response", + platformCause = throwable ) } return ApolloResponse.Builder(requestUuid = uuid4(), operation = operation) diff --git a/tests/data-builders-kotlin/src/test/kotlin/test/FragmentTest.kt b/tests/data-builders-kotlin/src/test/kotlin/test/FragmentTest.kt index f980f5af72..5ad243da60 100644 --- a/tests/data-builders-kotlin/src/test/kotlin/test/FragmentTest.kt +++ b/tests/data-builders-kotlin/src/test/kotlin/test/FragmentTest.kt @@ -1,6 +1,6 @@ package test -import com.apollographql.apollo3.exception.DefaultApolloException +import com.apollographql.apollo3.exception.NullOrMissingField import data.builders.fragment.AnimalDetailsImpl import data.builders.fragment.CatDetailsImpl import data.builders.fragment.TrivialFragmentImpl @@ -43,7 +43,7 @@ class FragmentTest { // __typename is unknown so this fails // XXX: we could be smarter about this (the parsers are // data.builders.fragment.AnimalDetailsImpl_ResponseAdapter$OnAnimal.fromJson(AnimalDetailsImpl_ResponseAdapter.kt:85) - assertFailsWith(DefaultApolloException::class) { + assertFailsWith(NullOrMissingField::class) { TrivialFragmentImpl.Data(Animal) { __typename = "Brontaroc" species = "alien" diff --git a/tests/http-cache/src/test/kotlin/HttpCacheTest.kt b/tests/http-cache/src/test/kotlin/HttpCacheTest.kt index 2dc9d9a53e..cf2f272f1e 100644 --- a/tests/http-cache/src/test/kotlin/HttpCacheTest.kt +++ b/tests/http-cache/src/test/kotlin/HttpCacheTest.kt @@ -1,10 +1,11 @@ + import com.apollographql.apollo3.ApolloClient import com.apollographql.apollo3.cache.http.HttpFetchPolicy import com.apollographql.apollo3.cache.http.httpCache import com.apollographql.apollo3.cache.http.httpExpireTimeout import com.apollographql.apollo3.cache.http.httpFetchPolicy import com.apollographql.apollo3.cache.http.isFromHttpCache -import com.apollographql.apollo3.exception.ApolloParseException +import com.apollographql.apollo3.exception.ApolloNetworkException import com.apollographql.apollo3.exception.HttpCacheMissException import com.apollographql.apollo3.mockserver.MockServer import com.apollographql.apollo3.mockserver.awaitRequest @@ -212,9 +213,9 @@ class HttpCacheTest { @Test fun incompleteJsonIsNotCached() = runTest(before = { before() }, after = { tearDown() }) { mockServer.enqueueString("""{"data":""") - assertIs( - apolloClient.query(GetRandomQuery()).execute().exception - ) + apolloClient.query(GetRandomQuery()).execute().exception.apply { + assertIs(this) + } // Should not have been cached assertIs( apolloClient.query(GetRandomQuery()).httpFetchPolicy(HttpFetchPolicy.CacheOnly).execute().exception