Skip to content

Commit ae455bf

Browse files
authored
fix: Fix remote error extensions (#240)
Error extensions from remote execution errors are now correctly propagated when stitching.
2 parents 601cef8 + 445c5b8 commit ae455bf

File tree

2 files changed

+66
-11
lines changed

2 files changed

+66
-11
lines changed

kgraphql-ktor-stitched/src/main/kotlin/com/apurebase/kgraphql/stitched/schema/execution/AbstractRemoteRequestExecutor.kt

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.apurebase.kgraphql.stitched.schema.execution
22

33
import com.apurebase.kgraphql.Context
44
import com.apurebase.kgraphql.ExperimentalAPI
5+
import com.apurebase.kgraphql.GraphQLError
56
import com.apurebase.kgraphql.GraphqlRequest
67
import com.apurebase.kgraphql.request.Variables
78
import com.apurebase.kgraphql.schema.execution.Execution
@@ -12,6 +13,7 @@ import com.apurebase.kgraphql.schema.structure.Field
1213
import com.apurebase.kgraphql.schema.structure.Type
1314
import com.apurebase.kgraphql.stitched.RemoteExecutionException
1415
import com.apurebase.kgraphql.stitched.StitchedGraphqlRequest
16+
import com.fasterxml.jackson.core.type.TypeReference
1517
import com.fasterxml.jackson.databind.JsonNode
1618
import com.fasterxml.jackson.databind.ObjectMapper
1719
import com.fasterxml.jackson.databind.node.ArrayNode
@@ -40,11 +42,24 @@ abstract class AbstractRemoteRequestExecutor(private val objectMapper: ObjectMap
4042
""".trimIndent()
4143
}
4244
val responseJson = objectMapper.readTree(response)
45+
// TODO: properly transfer errors from the remote execution
4346
responseJson["errors"]?.let { errors ->
44-
// TODO: properly transfer errors from the remote execution
45-
val messages = (errors as? ArrayNode)?.map { (it as? ObjectNode)?.get("message")?.textValue() }
46-
?: listOf("Error(s) during remote execution")
47-
throw RemoteExecutionException(message = messages.joinToString(", "), node = node)
47+
(errors as? ArrayNode)?.forEach { error ->
48+
val objectNode = error as? ObjectNode
49+
val message = objectNode?.get("message")?.textValue()?.takeIf { it.isNotBlank() }
50+
?: "Error(s) during remote execution"
51+
val extensionsNode = objectNode?.get("extensions") as? ObjectNode
52+
val extensions = mapOf("remoteUrl" to node.remoteUrl, "remoteOperation" to node.remoteOperation) +
53+
extensionsNode?.let {
54+
objectMapper.convertValue(it, object : TypeReference<Map<String, Any?>>() {})
55+
}.orEmpty()
56+
throw GraphQLError(
57+
message = message,
58+
nodes = listOf(node.selectionNode),
59+
extensions = extensions
60+
)
61+
}
62+
throw RemoteExecutionException(message = "Error(s) during remote execution", node = node)
4863
}
4964
return responseJson["data"]?.get(node.remoteOperation)
5065
}

kgraphql-ktor-stitched/src/test/kotlin/com/apurebase/kgraphql/stitched/schema/execution/StitchedSchemaExecutionTest.kt

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.apurebase.kgraphql.stitched.schema.execution
22

3+
import com.apurebase.kgraphql.BuiltInErrorCodes
34
import com.apurebase.kgraphql.ExperimentalAPI
45
import com.apurebase.kgraphql.GraphQL
6+
import com.apurebase.kgraphql.GraphQLError
57
import com.apurebase.kgraphql.GraphqlRequest
68
import com.apurebase.kgraphql.schema.dsl.SchemaBuilder
79
import com.apurebase.kgraphql.stitched.StitchedGraphQL
@@ -1691,7 +1693,7 @@ class StitchedSchemaExecutionTest {
16911693
{"data":{"remote2":{"bar2":42,"stitchedEnum":"REMOTE1"}}}
16921694
""".trimIndent()
16931695

1694-
// Call with *only* stiched fields should work (without complaining about missing selection sets)
1696+
// Call with *only* stitched fields should work (without complaining about missing selection sets)
16951697
client.post("local") {
16961698
header(HttpHeaders.ContentType, ContentType.Application.Json)
16971699
setBody(
@@ -2721,8 +2723,26 @@ class StitchedSchemaExecutionTest {
27212723

27222724
@Test
27232725
fun `errors from remote execution should be propagated correctly`() = testApplication {
2724-
fun SchemaBuilder.remoteSchema() = query("fail") {
2725-
resolver<String> { throw IllegalArgumentException("don't call me!") }
2726+
fun SchemaBuilder.remoteSchema() = run {
2727+
query("failRemote") {
2728+
resolver<String> {
2729+
throw GraphQLError(
2730+
message = "don't call me remote!",
2731+
extensions = mapOf(
2732+
"type" to BuiltInErrorCodes.BAD_USER_INPUT.name,
2733+
"remoteErrorKey" to listOf(
2734+
"remoteErrorValue1",
2735+
"remoteErrorValue2"
2736+
)
2737+
)
2738+
)
2739+
}
2740+
}
2741+
query("failRemote2") {
2742+
resolver<String> {
2743+
throw IllegalStateException()
2744+
}
2745+
}
27262746
}
27272747
install(GraphQL.FeatureInstance("KGraphql - Remote")) {
27282748
endpoint = "remote"
@@ -2737,8 +2757,14 @@ class StitchedSchemaExecutionTest {
27372757
remoteExecutor = TestRemoteRequestExecutor(client, objectMapper)
27382758
}
27392759
localSchema {
2740-
query("local") {
2741-
resolver { -> "local" }
2760+
query("failLocal") {
2761+
resolver<String> {
2762+
throw GraphQLError(
2763+
message = "don't call me local!",
2764+
extensionsErrorType = BuiltInErrorCodes.INTERNAL_SERVER_ERROR.name,
2765+
extensionsErrorDetail = mapOf("localErrorKey" to "localErrorValue")
2766+
)
2767+
}
27422768
}
27432769
}
27442770
remoteSchema("remote") {
@@ -2761,9 +2787,23 @@ class StitchedSchemaExecutionTest {
27612787
// TODO: should IMHO contain a data key according to https://spec.graphql.org/draft/#sec-Response-Format
27622788
client.post("local") {
27632789
header(HttpHeaders.ContentType, ContentType.Application.Json)
2764-
setBody(graphqlRequest("{ fail }"))
2790+
setBody(graphqlRequest("{ failLocal }"))
2791+
}.bodyAsText() shouldBeEqualTo """
2792+
{"errors":[{"message":"don't call me local!","locations":[],"path":[],"extensions":{"type":"INTERNAL_SERVER_ERROR","detail":{"localErrorKey":"localErrorValue"}}}]}
2793+
""".trimIndent()
2794+
2795+
client.post("local") {
2796+
header(HttpHeaders.ContentType, ContentType.Application.Json)
2797+
setBody(graphqlRequest("{ failRemote }"))
2798+
}.bodyAsText() shouldBeEqualTo """
2799+
{"errors":[{"message":"don't call me remote!","locations":[{"line":1,"column":3}],"path":[],"extensions":{"type":"BAD_USER_INPUT","remoteUrl":"remote","remoteOperation":"failRemote","remoteErrorKey":["remoteErrorValue1","remoteErrorValue2"]}}]}
2800+
""".trimIndent()
2801+
2802+
client.post("local") {
2803+
header(HttpHeaders.ContentType, ContentType.Application.Json)
2804+
setBody(graphqlRequest("{ failRemote2 }"))
27652805
}.bodyAsText() shouldBeEqualTo """
2766-
{"errors":[{"message":"don't call me!","locations":[{"line":1,"column":3}],"path":[],"extensions":{"type":"INTERNAL_SERVER_ERROR","detail":{"remoteUrl":"remote","remoteOperation":"fail"}}}]}
2806+
{"errors":[{"message":"Error(s) during remote execution","locations":[{"line":1,"column":3}],"path":[],"extensions":{"type":"INTERNAL_SERVER_ERROR","remoteUrl":"remote","remoteOperation":"failRemote2"}}]}
27672807
""".trimIndent()
27682808
}
27692809

0 commit comments

Comments
 (0)