diff --git a/docs/content/Plugins/ktor.md b/docs/content/Plugins/ktor.md index b8465e3f..d40e0f09 100644 --- a/docs/content/Plugins/ktor.md +++ b/docs/content/Plugins/ktor.md @@ -50,13 +50,12 @@ test it out directly within the browser. The GraphQL feature is extending the standard [KGraphQL configuration](../Reference/configuration.md) and providing its own set of configuration as described in the table below. -| Property | Description | Default value | -|--------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------| -| endpoint | This specifies what route will be delivering the GraphQL endpoint. When `playground` is enabled, it will use this endpoint also. | `/graphql` | -| context | Refer to example below | | -| wrap | If you want to wrap the route into something before KGraphQL will install the GraphQL route. You can use this wrapper. See example below for a more in depth on how to use it. | | -| errorHandler | Allows interaction with exceptions thrown during GraphQL execution and optional mapping to another one — in particular mapping to `GraphQLError` for serialization in the response. | | -| schema | This is where you are defining your schema. Please refer to [KGraphQL References](../Reference/operations.md) for further documentation on this. | ***required*** | +| Property | Description | Default value | +|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------| +| endpoint | This specifies what route will be delivering the GraphQL endpoint. When `playground` is enabled, it will use this endpoint also. | `/graphql` | +| context | Allows to add call-specific information to the GraphQL context, see example below. | | +| wrap | If you want to wrap the route into something before KGraphQL will install the GraphQL route. You can use this wrapper. See example below for a more in depth on how to use it. | | +| schema | This is where you are defining your schema. Please refer to [KGraphQL References](../Reference/operations.md) for further documentation on this. | ***required*** | ### Wrap @@ -74,7 +73,7 @@ This works great alongside the [context](#context) to provide a context to the K ### Context -To get access to the context +`context` allows to add call-specific information to the GraphQL context: === "Example" ```kotlin @@ -94,35 +93,6 @@ To get access to the context } ``` -### Error Handler - -By default, KGraphQL will wrap non-`GraphQLError` exceptions into an `ExecutionException` (when `wrapErrors = true`) -or rethrow them to be handled by Ktor (when `wrapErrors = false`). - -The `errorHandler` provides a way to **intercept and transform exceptions** before they are serialized. -It is always defined — by default it simply returns the same exception instance (`{ e -> e }`), -but you can override it to map specific exception types to `GraphQLError` or other `Throwable` instances. - -=== "Example" - ```kotlin - errorHandler { e -> - when (e) { - is ValidationException -> RequestError(e.message, extensions = mapOf("type" to "VALIDATION_ERROR")) - is DomainException -> RequestError(e.message, extensions = mapOf("type" to "DOMAIN_ERROR")) - is GraphQLError -> e - else -> ExecutionException(e.message ?: "Unknown execution error", cause = e) - } - } - schema { - query("hello") { - resolver { ctx: Context -> - val user = ctx.get()!! - "Hello ${user.name}" - } - } - } - ``` - ## Schema Definition Language (SDL) The [Schema Definition Language](https://graphql.org/learn/schema/#type-language) (or Type System Definition Language) is a human-readable, language-agnostic diff --git a/docs/content/Reference/configuration.md b/docs/content/Reference/configuration.md index c6326865..94d8058c 100644 --- a/docs/content/Reference/configuration.md +++ b/docs/content/Reference/configuration.md @@ -1,16 +1,17 @@ KGraphQL schema allows configuration of the following properties: -| Property | Description | Default value | -|--------------------------------|-----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| useDefaultPrettyPrinter | Schema pretty prints JSON responses | `false` | -| useCachingDocumentParser | Schema caches parsed query documents | `true` | -| documentParserCacheMaximumSize | Schema document cache maximum size | `1000` | -| objectMapper | Schema is using Jackson ObjectMapper from this property | result of `jacksonObjectMapper()` from [jackson-kotlin-module](https://github.com/FasterXML/jackson-module-kotlin) | -| acceptSingleValueAsArray | Schema accepts single argument values as singleton list | `true` | -| coroutineDispatcher | Schema is using CoroutineDispatcher from this property | [Dispatchers.Default](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/jvm/src/Dispatchers.kt) | -| genericTypeResolver | Schema is using generic type resolver from this property | [GenericTypeResolver.DEFAULT](https://github.com/stuebingerb/KGraphQL/blob/main/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/GenericTypeResolver.kt) | -| wrapErrors | Schema wraps exceptions from resolvers as GraphQLError | | -| introspection | Schema allows introspection (also affects SDL). Introspection can be disabled in production to reduce attack surface. | `true` | +| Property | Description | Default value | +|--------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| useDefaultPrettyPrinter | Schema pretty prints JSON responses | `false` | +| useCachingDocumentParser | Schema caches parsed query documents | `true` | +| documentParserCacheMaximumSize | Schema document cache maximum size | `1000` | +| objectMapper | Schema is using Jackson ObjectMapper from this property | result of `jacksonObjectMapper()` from [jackson-kotlin-module](https://github.com/FasterXML/jackson-module-kotlin) | +| acceptSingleValueAsArray | Schema accepts single argument values as singleton list | `true` | +| coroutineDispatcher | Schema is using CoroutineDispatcher from this property | [Dispatchers.Default](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/jvm/src/Dispatchers.kt) | +| genericTypeResolver | Schema is using generic type resolver from this property | [GenericTypeResolver.DEFAULT](https://github.com/stuebingerb/KGraphQL/blob/main/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/GenericTypeResolver.kt) | +| wrapErrors | Schema wraps exceptions from resolvers as GraphQLError | | +| introspection | Schema allows introspection (also affects SDL). Introspection can be disabled in production to reduce attack surface. | `true` | +| errorHandler | Allows interaction with exceptions thrown during GraphQL execution and optional mapping to another one — in particular mapping to `GraphQLError` for serialization in the response. | [ErrorHandler](https://github.com/stuebingerb/KGraphQL/blob/main/kgraphql/src/main/kotlin/com/stuebingerb/kgraphql/schema/execution/ErrorHandler.kt) | === "Example" ```kotlin diff --git a/docs/content/Reference/errorHandling.md b/docs/content/Reference/errorHandling.md index eab43bea..2f8d8632 100644 --- a/docs/content/Reference/errorHandling.md +++ b/docs/content/Reference/errorHandling.md @@ -221,4 +221,41 @@ Those re-thrown exceptions could then be handled with the [`StatusPages` Ktor pl Invalid input: java.lang.IllegalArgumentException: Illegal argument ``` -Because exceptions are re-thrown, `wrapErrors = false` can never result in partial responses. +Because exceptions are re-thrown, `wrapErrors = false` can never result in partial responses. `wrapErrors = false` will +also not invoke a custom error handler. If you want to throw exceptions with custom mapping, use `wrapErrors = true` and +re-throw mapped exceptions from the error handler. + +## Error Handler + +In KGraphQL, the schema can [configure a custom _error handler_](configuration.md) that is called for each exception +encountered during execution. It can be used to customize default error mapping, and to add additional extensions to +the response. + +The error handler is supposed to return a subclass of `GraphQLError`, which is either a `RequestError` or an `ExecutionError` +that will be handled according to the schema. When subclassing from the default `ErrorHandler`, mapping can be delegated +to the standard implementation, completely replaced, or a mixture of both. + +=== "Example" + ```kotlin + val customErrorHandler = object : ErrorHandler() { + override suspend fun handleException(ctx: Context, node: Execution.Node, exception: Throwable): GraphQLError { + return when (exception) { + is IllegalArgumentException -> ExecutionError( + message = exception.message ?: "", + node = node, + extensions = mapOf("type" to "CUSTOM_ERROR_TYPE") + ) + + is IllegalAccessException -> RequestError( + message = "You shall not pass!", + node = node.selectionNode, + extensions = mapOf("required_role" to "ADMIN", "reason" to "Gandalf") + ) + + else -> super.handleException(ctx, node, exception) + } + } + } + ``` + +(!) Exceptions from the error handler itself are *not* wrapped, regardless of the `wrapErrors` configuration. diff --git a/kgraphql-ktor-stitched/api/kgraphql-ktor-stitched.api b/kgraphql-ktor-stitched/api/kgraphql-ktor-stitched.api index 6f539681..7ebb9aa8 100644 --- a/kgraphql-ktor-stitched/api/kgraphql-ktor-stitched.api +++ b/kgraphql-ktor-stitched/api/kgraphql-ktor-stitched.api @@ -56,8 +56,8 @@ public final class com/apurebase/kgraphql/stitched/StitchedKGraphQL { } public final class com/apurebase/kgraphql/stitched/schema/configuration/StitchedSchemaConfiguration : com/apurebase/kgraphql/configuration/SchemaConfiguration { - public fun (ZJLcom/fasterxml/jackson/databind/ObjectMapper;ZLkotlinx/coroutines/CoroutineDispatcher;ZZLcom/apurebase/kgraphql/schema/execution/GenericTypeResolver;Lcom/apurebase/kgraphql/schema/execution/ArgumentTransformer;Lcom/apurebase/kgraphql/stitched/schema/execution/RemoteRequestExecutor;Ljava/lang/String;)V - public synthetic fun (ZJLcom/fasterxml/jackson/databind/ObjectMapper;ZLkotlinx/coroutines/CoroutineDispatcher;ZZLcom/apurebase/kgraphql/schema/execution/GenericTypeResolver;Lcom/apurebase/kgraphql/schema/execution/ArgumentTransformer;Lcom/apurebase/kgraphql/stitched/schema/execution/RemoteRequestExecutor;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (ZJLcom/fasterxml/jackson/databind/ObjectMapper;ZLkotlinx/coroutines/CoroutineDispatcher;ZZLcom/apurebase/kgraphql/schema/execution/GenericTypeResolver;Lcom/apurebase/kgraphql/schema/execution/ArgumentTransformer;Lcom/apurebase/kgraphql/schema/execution/ErrorHandler;Lcom/apurebase/kgraphql/stitched/schema/execution/RemoteRequestExecutor;Ljava/lang/String;)V + public synthetic fun (ZJLcom/fasterxml/jackson/databind/ObjectMapper;ZLkotlinx/coroutines/CoroutineDispatcher;ZZLcom/apurebase/kgraphql/schema/execution/GenericTypeResolver;Lcom/apurebase/kgraphql/schema/execution/ArgumentTransformer;Lcom/apurebase/kgraphql/schema/execution/ErrorHandler;Lcom/apurebase/kgraphql/stitched/schema/execution/RemoteRequestExecutor;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getLocalUrl ()Ljava/lang/String; public final fun getRemoteExecutor ()Lcom/apurebase/kgraphql/stitched/schema/execution/RemoteRequestExecutor; } diff --git a/kgraphql-ktor-stitched/src/main/kotlin/com/apurebase/kgraphql/stitched/schema/configuration/StitchedSchemaConfiguration.kt b/kgraphql-ktor-stitched/src/main/kotlin/com/apurebase/kgraphql/stitched/schema/configuration/StitchedSchemaConfiguration.kt index cedda5d7..75a29a1f 100644 --- a/kgraphql-ktor-stitched/src/main/kotlin/com/apurebase/kgraphql/stitched/schema/configuration/StitchedSchemaConfiguration.kt +++ b/kgraphql-ktor-stitched/src/main/kotlin/com/apurebase/kgraphql/stitched/schema/configuration/StitchedSchemaConfiguration.kt @@ -3,6 +3,7 @@ package com.apurebase.kgraphql.stitched.schema.configuration import com.apurebase.kgraphql.ExperimentalAPI import com.apurebase.kgraphql.configuration.SchemaConfiguration import com.apurebase.kgraphql.schema.execution.ArgumentTransformer +import com.apurebase.kgraphql.schema.execution.ErrorHandler import com.apurebase.kgraphql.schema.execution.GenericTypeResolver import com.apurebase.kgraphql.stitched.schema.execution.RemoteRequestExecutor import com.fasterxml.jackson.databind.ObjectMapper @@ -23,6 +24,7 @@ class StitchedSchemaConfiguration( introspection: Boolean = true, genericTypeResolver: GenericTypeResolver, argumentTransformer: ArgumentTransformer, + errorHandler: ErrorHandler, val remoteExecutor: RemoteRequestExecutor, val localUrl: String? ) : SchemaConfiguration( @@ -34,5 +36,6 @@ class StitchedSchemaConfiguration( wrapErrors, introspection, genericTypeResolver, - argumentTransformer + argumentTransformer, + errorHandler ) diff --git a/kgraphql-ktor-stitched/src/main/kotlin/com/apurebase/kgraphql/stitched/schema/dsl/StitchedSchemaConfigurationDSL.kt b/kgraphql-ktor-stitched/src/main/kotlin/com/apurebase/kgraphql/stitched/schema/dsl/StitchedSchemaConfigurationDSL.kt index 8efa03a5..f89afdec 100644 --- a/kgraphql-ktor-stitched/src/main/kotlin/com/apurebase/kgraphql/stitched/schema/dsl/StitchedSchemaConfigurationDSL.kt +++ b/kgraphql-ktor-stitched/src/main/kotlin/com/apurebase/kgraphql/stitched/schema/dsl/StitchedSchemaConfigurationDSL.kt @@ -27,6 +27,7 @@ open class StitchedSchemaConfigurationDSL : SchemaConfigurationDSL() { introspection = introspection, genericTypeResolver = genericTypeResolver, argumentTransformer = RemoteArgumentTransformer(objectMapper, genericTypeResolver), + errorHandler = errorHandler, remoteExecutor = requireNotNull(remoteExecutor) { "Remote executor not defined" }, localUrl = localUrl ) diff --git a/kgraphql-ktor/api/kgraphql-ktor.api b/kgraphql-ktor/api/kgraphql-ktor.api index f957bcda..de4a86b3 100644 --- a/kgraphql-ktor/api/kgraphql-ktor.api +++ b/kgraphql-ktor/api/kgraphql-ktor.api @@ -7,7 +7,6 @@ public final class com/apurebase/kgraphql/GraphQL { public final class com/apurebase/kgraphql/GraphQL$Configuration : com/apurebase/kgraphql/schema/dsl/SchemaConfigurationDSL { public fun ()V public final fun context (Lkotlin/jvm/functions/Function2;)V - public final fun errorHandler (Lkotlin/jvm/functions/Function1;)V public final fun getEndpoint ()Ljava/lang/String; public final fun getPlayground ()Z public final fun schema (Lkotlin/jvm/functions/Function1;)V diff --git a/kgraphql-ktor/src/main/kotlin/com/apurebase/kgraphql/KtorFeature.kt b/kgraphql-ktor/src/main/kotlin/com/apurebase/kgraphql/KtorFeature.kt index 1719d11a..c89c3401 100644 --- a/kgraphql-ktor/src/main/kotlin/com/apurebase/kgraphql/KtorFeature.kt +++ b/kgraphql-ktor/src/main/kotlin/com/apurebase/kgraphql/KtorFeature.kt @@ -45,13 +45,8 @@ class GraphQL(val schema: Schema) { wrapWith = block } - fun errorHandler(block: (e: Throwable) -> Throwable) { - errorHandler = block - } - internal var contextSetup: (ContextBuilder.(ApplicationCall) -> Unit)? = null internal var wrapWith: (Route.(next: Route.() -> Unit) -> Unit)? = null - internal var errorHandler: ((Throwable) -> Throwable) = { e -> e } internal var schemaBlock: (SchemaBuilder.() -> Unit)? = null } @@ -121,14 +116,9 @@ class GraphQL(val schema: Schema) { coroutineScope { proceed() } - } catch (e: Throwable) { - val error = config.errorHandler(e) - if (error !is GraphQLError) { - throw e - } - + } catch (e: GraphQLError) { context.respondText( - error.serialize(), + e.serialize(), ContentType.Application.Json, HttpStatusCode.OK ) diff --git a/kgraphql-ktor/src/test/kotlin/com/apurebase/kgraphql/KtorFeatureTest.kt b/kgraphql-ktor/src/test/kotlin/com/apurebase/kgraphql/KtorFeatureTest.kt index 4ab350a7..2f5fcf93 100644 --- a/kgraphql-ktor/src/test/kotlin/com/apurebase/kgraphql/KtorFeatureTest.kt +++ b/kgraphql-ktor/src/test/kotlin/com/apurebase/kgraphql/KtorFeatureTest.kt @@ -253,31 +253,6 @@ class KtorFeatureTest : KtorTest() { } } - @Test - fun `should work with error handler`() { - val errorHandler: (Throwable) -> GraphQLError = { e -> - RequestError( - message = e.message ?: "unknown", - node = null, - extensions = mapOf("type" to BuiltInErrorCodes.INTERNAL_SERVER_ERROR.name) - ) - } - - val server = withServer(errorHandler = errorHandler) { - query("error") { - resolver { throw Exception("Error message") } - } - } - - val response = server("query") { - field("error") - } - runBlocking { - response.bodyAsText() shouldBe "{\"errors\":[{\"message\":\"Error message\",\"locations\":[{\"line\":2,\"column\":1}],\"path\":[\"error\"],\"extensions\":{\"type\":\"INTERNAL_SERVER_ERROR\"}}],\"data\":null}" - response.contentType() shouldBe ContentType.Application.Json - } - } - @Test fun `should work without error handler`() { val server = withServer { diff --git a/kgraphql-ktor/src/test/kotlin/com/apurebase/kgraphql/KtorTest.kt b/kgraphql-ktor/src/test/kotlin/com/apurebase/kgraphql/KtorTest.kt index 261beb3b..0bf616d7 100644 --- a/kgraphql-ktor/src/test/kotlin/com/apurebase/kgraphql/KtorTest.kt +++ b/kgraphql-ktor/src/test/kotlin/com/apurebase/kgraphql/KtorTest.kt @@ -18,7 +18,6 @@ open class KtorTest { fun withServer( ctxBuilder: ContextBuilder.(ApplicationCall) -> Unit = {}, authHeader: String? = null, - errorHandler: ((Throwable) -> GraphQLError)? = null, wrapErrors: Boolean? = null, block: SchemaBuilder.() -> Unit ): (String, Kraph.() -> Unit) -> HttpResponse { @@ -38,7 +37,6 @@ open class KtorTest { wrap { next -> authenticate(optional = authHeader == null) { next() } } - errorHandler?.let { this.errorHandler(it) } wrapErrors?.let { this.wrapErrors = it } schema(block) } diff --git a/kgraphql/api/kgraphql.api b/kgraphql/api/kgraphql.api index 6f0fd667..1267a66a 100644 --- a/kgraphql/api/kgraphql.api +++ b/kgraphql/api/kgraphql.api @@ -88,11 +88,12 @@ public abstract interface class com/apurebase/kgraphql/configuration/PluginConfi } public class com/apurebase/kgraphql/configuration/SchemaConfiguration { - public fun (ZJLcom/fasterxml/jackson/databind/ObjectMapper;ZLkotlinx/coroutines/CoroutineDispatcher;ZZLcom/apurebase/kgraphql/schema/execution/GenericTypeResolver;Lcom/apurebase/kgraphql/schema/execution/ArgumentTransformer;)V - public synthetic fun (ZJLcom/fasterxml/jackson/databind/ObjectMapper;ZLkotlinx/coroutines/CoroutineDispatcher;ZZLcom/apurebase/kgraphql/schema/execution/GenericTypeResolver;Lcom/apurebase/kgraphql/schema/execution/ArgumentTransformer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (ZJLcom/fasterxml/jackson/databind/ObjectMapper;ZLkotlinx/coroutines/CoroutineDispatcher;ZZLcom/apurebase/kgraphql/schema/execution/GenericTypeResolver;Lcom/apurebase/kgraphql/schema/execution/ArgumentTransformer;Lcom/apurebase/kgraphql/schema/execution/ErrorHandler;)V + public synthetic fun (ZJLcom/fasterxml/jackson/databind/ObjectMapper;ZLkotlinx/coroutines/CoroutineDispatcher;ZZLcom/apurebase/kgraphql/schema/execution/GenericTypeResolver;Lcom/apurebase/kgraphql/schema/execution/ArgumentTransformer;Lcom/apurebase/kgraphql/schema/execution/ErrorHandler;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun getArgumentTransformer ()Lcom/apurebase/kgraphql/schema/execution/ArgumentTransformer; public final fun getCoroutineDispatcher ()Lkotlinx/coroutines/CoroutineDispatcher; public final fun getDocumentParserCacheMaximumSize ()J + public final fun getErrorHandler ()Lcom/apurebase/kgraphql/schema/execution/ErrorHandler; public final fun getGenericTypeResolver ()Lcom/apurebase/kgraphql/schema/execution/GenericTypeResolver; public final fun getIntrospection ()Z public final fun getObjectMapper ()Lcom/fasterxml/jackson/databind/ObjectMapper; @@ -547,6 +548,7 @@ public class com/apurebase/kgraphql/schema/dsl/SchemaConfigurationDSL { public final fun getAcceptSingleValueAsArray ()Z public final fun getCoroutineDispatcher ()Lkotlinx/coroutines/CoroutineDispatcher; public final fun getDocumentParserCacheMaximumSize ()J + public final fun getErrorHandler ()Lcom/apurebase/kgraphql/schema/execution/ErrorHandler; public final fun getGenericTypeResolver ()Lcom/apurebase/kgraphql/schema/execution/GenericTypeResolver; public final fun getIntrospection ()Z public final fun getObjectMapper ()Lcom/fasterxml/jackson/databind/ObjectMapper; @@ -556,6 +558,7 @@ public class com/apurebase/kgraphql/schema/dsl/SchemaConfigurationDSL { public final fun setAcceptSingleValueAsArray (Z)V public final fun setCoroutineDispatcher (Lkotlinx/coroutines/CoroutineDispatcher;)V public final fun setDocumentParserCacheMaximumSize (J)V + public final fun setErrorHandler (Lcom/apurebase/kgraphql/schema/execution/ErrorHandler;)V public final fun setGenericTypeResolver (Lcom/apurebase/kgraphql/schema/execution/GenericTypeResolver;)V public final fun setIntrospection (Z)V public final fun setObjectMapper (Lcom/fasterxml/jackson/databind/ObjectMapper;)V @@ -775,6 +778,11 @@ public class com/apurebase/kgraphql/schema/execution/DefaultGenericTypeResolver public fun unbox (Ljava/lang/Object;)Ljava/lang/Object; } +public class com/apurebase/kgraphql/schema/execution/ErrorHandler { + public fun ()V + public fun handleException (Lcom/apurebase/kgraphql/Context;Lcom/apurebase/kgraphql/schema/execution/Execution$Node;Ljava/lang/Throwable;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + public abstract class com/apurebase/kgraphql/schema/execution/Execution { public abstract fun getDirectives ()Ljava/util/Map; public abstract fun getFullPath ()Ljava/util/List; diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/configuration/SchemaConfiguration.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/configuration/SchemaConfiguration.kt index b2282fcf..8e98b989 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/configuration/SchemaConfiguration.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/configuration/SchemaConfiguration.kt @@ -1,6 +1,7 @@ package com.apurebase.kgraphql.configuration import com.apurebase.kgraphql.schema.execution.ArgumentTransformer +import com.apurebase.kgraphql.schema.execution.ErrorHandler import com.apurebase.kgraphql.schema.execution.GenericTypeResolver import com.fasterxml.jackson.databind.ObjectMapper import kotlinx.coroutines.CoroutineDispatcher @@ -18,5 +19,6 @@ open class SchemaConfiguration( // allow schema introspection val introspection: Boolean = true, val genericTypeResolver: GenericTypeResolver, - val argumentTransformer: ArgumentTransformer + val argumentTransformer: ArgumentTransformer, + val errorHandler: ErrorHandler ) diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/SchemaConfigurationDSL.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/SchemaConfigurationDSL.kt index 486d0e53..06c02145 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/SchemaConfigurationDSL.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/SchemaConfigurationDSL.kt @@ -2,6 +2,7 @@ package com.apurebase.kgraphql.schema.dsl import com.apurebase.kgraphql.configuration.SchemaConfiguration import com.apurebase.kgraphql.schema.execution.ArgumentTransformer +import com.apurebase.kgraphql.schema.execution.ErrorHandler import com.apurebase.kgraphql.schema.execution.GenericTypeResolver import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper @@ -19,6 +20,7 @@ open class SchemaConfigurationDSL { var wrapErrors: Boolean = true var introspection: Boolean = true var genericTypeResolver: GenericTypeResolver = GenericTypeResolver.DEFAULT + var errorHandler: ErrorHandler = ErrorHandler() fun update(block: SchemaConfigurationDSL.() -> Unit) = block() open fun build(): SchemaConfiguration { @@ -32,7 +34,8 @@ open class SchemaConfigurationDSL { wrapErrors = wrapErrors, introspection = introspection, genericTypeResolver = genericTypeResolver, - argumentTransformer = ArgumentTransformer(genericTypeResolver) + argumentTransformer = ArgumentTransformer(genericTypeResolver), + errorHandler = errorHandler ) } } diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ErrorHandler.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ErrorHandler.kt new file mode 100644 index 00000000..8fa87963 --- /dev/null +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ErrorHandler.kt @@ -0,0 +1,26 @@ +package com.apurebase.kgraphql.schema.execution + +import com.apurebase.kgraphql.Context +import com.apurebase.kgraphql.ExecutionError +import com.apurebase.kgraphql.GraphQLError + +/** + * Error handler used to transform [Throwable]s encountered during execution into [GraphQLError]s included in + * the response. Default implementation will leave existing GraphQL errors as-is, and wrap everything else as + * [ExecutionError]. + * + * Intended to be subclassed for custom error handling while preserving access to default mapping. + * + * Note that exceptions caused during [handleException] are *not* wrapped, and will abort execution. + */ +open class ErrorHandler { + open suspend fun handleException(ctx: Context, node: Execution.Node, exception: Throwable): GraphQLError = + when (exception) { + is GraphQLError -> exception + else -> ExecutionError( + exception.message ?: exception::class.simpleName ?: "Error during execution", + node, + exception + ) + } +} diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ParallelRequestExecutor.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ParallelRequestExecutor.kt index 8eb07fd7..24ee015c 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ParallelRequestExecutor.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ParallelRequestExecutor.kt @@ -468,26 +468,27 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor { return createNode(ctx, value, node, field.returnType) } - private fun handleException( + private suspend fun handleException( ctx: ExecutionContext, node: Execution.Node, returnType: Type, exception: Throwable ): CompletableDeferred { - if (!schema.configuration.wrapErrors || exception is RequestError || exception is CancellationException) { + if (!schema.configuration.wrapErrors || exception is CancellationException) { throw exception } - val error = exception as? ExecutionError ?: ExecutionError( - exception.message ?: exception::class.simpleName ?: "Error during execution", - node, - exception - ) - if (returnType.isNullable()) { - ctx.requestContext.raiseError(error) - return CompletableDeferred(createNullNode(node, returnType)) - } else { - // Propagate error to parent - throw error + + when (val handledError = schema.configuration.errorHandler.handleException(ctx.requestContext, node, exception)) { + is RequestError -> throw handledError + is ExecutionError -> { + if (returnType.isNullable()) { + ctx.requestContext.raiseError(handledError) + return CompletableDeferred(createNullNode(node, returnType)) + } else { + // Propagate error to parent + throw handledError + } + } } } diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/QueryTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/QueryTest.kt index a05fcbab..a0797c41 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/QueryTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/QueryTest.kt @@ -6,6 +6,7 @@ import com.apurebase.kgraphql.Director import com.apurebase.kgraphql.ExecutionError import com.apurebase.kgraphql.ExecutionException import com.apurebase.kgraphql.Film +import com.apurebase.kgraphql.GraphQLError import com.apurebase.kgraphql.Id import com.apurebase.kgraphql.KGraphQL import com.apurebase.kgraphql.RequestError @@ -18,6 +19,7 @@ import com.apurebase.kgraphql.expectExecutionError import com.apurebase.kgraphql.expectRequestError import com.apurebase.kgraphql.extract import com.apurebase.kgraphql.helpers.getFields +import com.apurebase.kgraphql.schema.execution.ErrorHandler import com.apurebase.kgraphql.schema.execution.Execution import com.apurebase.kgraphql.schema.scalar.ID import io.kotest.matchers.shouldBe @@ -1188,4 +1190,47 @@ class QueryTest : BaseSchemaTest() { {"errors":[{"message":"Cannot get item 'missing'","locations":[{"line":1,"column":3}],"path":["items"],"extensions":{"type":"NOT_FOUND","reason":"Item is missing"}}],"data":{"items":[{"data":"Existing 1"},{"data":"Existing 2"}]}} """.trimIndent() } + + @Test + fun `should work with custom error handler`() { + val customErrorHandler = object : ErrorHandler() { + override suspend fun handleException(ctx: Context, node: Execution.Node, exception: Throwable): GraphQLError { + return when (exception) { + is IllegalArgumentException -> ExecutionError( + message = exception.message ?: "", + node = node, + extensions = mapOf("type" to "CUSTOM_ERROR_TYPE") + ) + + is IllegalAccessException -> RequestError( + message = "You shall not pass!", + node = node.selectionNode, + extensions = mapOf("required_role" to "ADMIN", "reason" to "Gandalf") + ) + + else -> super.handleException(ctx, node, exception) + } + } + } + + val schema = KGraphQL.schema { + configure { + errorHandler = customErrorHandler + } + query("executionError") { + resolver { throw IllegalArgumentException("Illegal argument") } + } + query("requestError") { + resolver { throw IllegalAccessException() } + } + } + + schema.executeBlocking("{ executionError }") shouldBe """ + {"errors":[{"message":"Illegal argument","locations":[{"line":1,"column":3}],"path":["executionError"],"extensions":{"type":"CUSTOM_ERROR_TYPE"}}],"data":{"executionError":null}} + """.trimIndent() + + schema.executeBlocking("{ requestError }") shouldBe """ + {"errors":[{"message":"You shall not pass!","locations":[{"line":1,"column":3}],"extensions":{"required_role":"ADMIN","reason":"Gandalf"}}]} + """.trimIndent() + } }