Skip to content

feat!: support partial responses#610

Open
stuebingerb wants to merge 1 commit intomainfrom
feature/support-partial-responses
Open

feat!: support partial responses#610
stuebingerb wants to merge 1 commit intomainfrom
feature/support-partial-responses

Conversation

@stuebingerb
Copy link
Copy Markdown
Owner

Adds support for proper error handling and partial responses as defined by the spec, cf. https://spec.graphql.org/September2025/#sec-Errors

KGraphQL now distinguishes between request and execution errors, and the latter now result in a response that contains both, data and errors. Execution errors result in a value of null for nullable fields, and will bubble up to their parent node for non-nullable fields. Additionally, resolvers can now raise execution errors while still returning data.

If data and errors are present in the response, the errors key is serialized first to make it more apparent that errors are present.

Behavior for request errors is unchanged, and wrapErrors configuration is still supported for now - but likely subject to change in the future.

Resolves #114

BREAKING CHANGE: relying on exceptions being thrown from query execution may no longer work as before. It is advised to specify wrapErrors = false and report an issue with the respective use case.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 15, 2026

Codecov Report

❌ Patch coverage is 86.46288% with 31 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.09%. Comparing base (e6969de) to head (c70d308).

Files with missing lines Patch % Lines
...raphql/schema/execution/ParallelRequestExecutor.kt 93.49% 3 Missing and 5 partials ⚠️
...e/kgraphql/schema/execution/ArgumentTransformer.kt 73.07% 7 Missing ⚠️
...n/com/apurebase/kgraphql/schema/scalar/Coercion.kt 22.22% 6 Missing and 1 partial ⚠️
.../schema/execution/AbstractRemoteRequestExecutor.kt 88.88% 0 Missing and 3 partials ⚠️
...purebase/kgraphql/schema/builtin/BuiltInScalars.kt 80.00% 0 Missing and 3 partials ⚠️
...m/apurebase/kgraphql/helpers/KGraphQLExtensions.kt 83.33% 0 Missing and 2 partials ⚠️
...lin/com/apurebase/kgraphql/schema/DefaultSchema.kt 90.90% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #610      +/-   ##
==========================================
+ Coverage   84.00%   84.09%   +0.08%     
==========================================
  Files         152      151       -1     
  Lines        4978     4999      +21     
  Branches      861      861              
==========================================
+ Hits         4182     4204      +22     
  Misses        495      495              
+ Partials      301      300       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Adds support for proper error handling and partial responses as
defined by the spec, cf. https://spec.graphql.org/September2025/#sec-Errors

KGraphQL now distinguishes between _request_ and _execution_ errors,
and the latter now result in a response that contains both, `data` and
`errors`. Execution errors result in a value of `null` for nullable
fields, and will bubble up to their parent node for non-nullable
fields. Additionally, resolvers can now raise execution errors while
still returning data.

If data _and_ errors are present in the response, the `errors` key is
serialized first to make it more apparent that errors are present.

Behavior for request errors is unchanged, and `wrapErrors` configuration
is still supported for now - but likely subject to change in the
future.

Resolves #114

BREAKING CHANGE: relying on exceptions being thrown from query
execution may no longer work as before. It is advised to specify
`wrapErrors = false` and report an issue with the respective use case.
@stuebingerb stuebingerb force-pushed the feature/support-partial-responses branch from acea407 to c70d308 Compare April 15, 2026 13:37
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 15, 2026

🐰 Bencher Report

Branchfeature/support-partial-responses
Testbedubuntu-latest

🚨 1 Alert

BenchmarkMeasure
Units
ViewBenchmark Result
(Result Δ%)
Lower Boundary
(Limit %)
com.apurebase.kgraphql.RequestCachingBenchmark.invalidRequestThroughput
operations / second (ops/s) x 1e3
📈 plot
🚷 threshold
🚨 alert (🔔)
144.48 ops/s x 1e3
(-35.87%)Baseline: 225.31 ops/s x 1e3
208.95 ops/s x 1e3
(144.62%)

Click to view all benchmark results
BenchmarkThroughputBenchmark Result
operations / second (ops/s)
(Result Δ%)
Lower Boundary
operations / second (ops/s)
(Limit %)
com.apurebase.kgraphql.FunctionExecutionBenchmark.benchmarkFunctionExecution📈 view plot
🚷 view threshold
5,976,206.90 ops/s
(+1.81%)Baseline: 5,869,764.25 ops/s
5,212,577.41 ops/s
(87.22%)
com.apurebase.kgraphql.ParallelExecutionBenchmark.queryBenchmark📈 view plot
🚷 view threshold
1.30 ops/s
(-0.20%)Baseline: 1.30 ops/s
1.30 ops/s
(99.99%)
com.apurebase.kgraphql.QueryBenchmark.executionError📈 view plot
🚷 view threshold
16,914.29 ops/s
(+0.32%)Baseline: 16,859.58 ops/s
11,655.17 ops/s
(68.91%)
com.apurebase.kgraphql.QueryBenchmark.inputFromDocument📈 view plot
🚷 view threshold
19,640.40 ops/s
(-9.70%)Baseline: 21,749.82 ops/s
16,054.30 ops/s
(81.74%)
com.apurebase.kgraphql.QueryBenchmark.inputFromVariable📈 view plot
🚷 view threshold
18,813.68 ops/s
(-8.99%)Baseline: 20,671.38 ops/s
15,516.15 ops/s
(82.47%)
com.apurebase.kgraphql.QueryBenchmark.largeList📈 view plot
🚷 view threshold
4.73 ops/s
(+5.57%)Baseline: 4.48 ops/s
3.89 ops/s
(82.26%)
com.apurebase.kgraphql.QueryBenchmark.largeListWithFragment📈 view plot
🚷 view threshold
5.27 ops/s
(+6.35%)Baseline: 4.95 ops/s
4.32 ops/s
(82.05%)
com.apurebase.kgraphql.QueryBenchmark.manyChildren📈 view plot
🚷 view threshold
179.77 ops/s
(-4.28%)Baseline: 187.81 ops/s
156.12 ops/s
(86.84%)
com.apurebase.kgraphql.QueryBenchmark.manyChildrenWithFragment📈 view plot
🚷 view threshold
197.91 ops/s
(-6.83%)Baseline: 212.42 ops/s
175.03 ops/s
(88.44%)
com.apurebase.kgraphql.QueryBenchmark.manyDataChildren📈 view plot
🚷 view threshold
8.92 ops/s
(-0.22%)Baseline: 8.94 ops/s
8.84 ops/s
(99.15%)
com.apurebase.kgraphql.QueryBenchmark.manyOperations📈 view plot
🚷 view threshold
302.12 ops/s
(+7.55%)Baseline: 280.91 ops/s
227.24 ops/s
(75.21%)
com.apurebase.kgraphql.QueryBenchmark.manyOperationsWithFragment📈 view plot
🚷 view threshold
309.90 ops/s
(+7.86%)Baseline: 287.30 ops/s
231.29 ops/s
(74.63%)
com.apurebase.kgraphql.QueryBenchmark.nestedObject📈 view plot
🚷 view threshold
7,314.75 ops/s
(-1.78%)Baseline: 7,447.16 ops/s
6,235.89 ops/s
(85.25%)
com.apurebase.kgraphql.RequestCachingBenchmark.invalidRequest📈 view plot
🚷 view threshold
🚨 view alert (🔔)
144,480.21 ops/s
(-35.87%)Baseline: 225,308.89 ops/s
208,951.99 ops/s
(144.62%)

com.apurebase.kgraphql.RequestCachingBenchmark.largeRequest📈 view plot
🚷 view threshold
7,437.61 ops/s
(-3.47%)Baseline: 7,705.19 ops/s
6,484.10 ops/s
(87.18%)
com.apurebase.kgraphql.RequestCachingBenchmark.smallRequest📈 view plot
🚷 view threshold
10,197.22 ops/s
(-9.36%)Baseline: 11,250.69 ops/s
9,170.66 ops/s
(89.93%)
com.apurebase.kgraphql.SimpleExecutionOverheadBenchmark.benchmark📈 view plot
🚷 view threshold
474,226.98 ops/s
(+0.57%)Baseline: 471,558.92 ops/s
452,575.87 ops/s
(95.43%)
🐰 View full continuous benchmarking report in Bencher

@stuebingerb
Copy link
Copy Markdown
Owner Author

stuebingerb commented Apr 15, 2026

This is part 3 of revisited error handling after #560 and #567.
The next pull request (#611) will move the current error handler from Ktor to the schema itself and use it during handleException. Another (future) pull request may then remove wrapErrors in favor of GraphQL-over-HTTP.

@stuebingerb
Copy link
Copy Markdown
Owner Author

@grumpy-programmer Sorry, this took way longer than anticipated but - following our conversation in #437 - if you're still interested in discussing error handling then I'd be happy to receive your thoughts here and in the linked pull requests 🙂

@grumpy-programmer
Copy link
Copy Markdown
Contributor

@stuebingerb sure, happy to help. I'll check it out at the weekend, if you don't mind, because this is beefy PR 😄

@stuebingerb
Copy link
Copy Markdown
Owner Author

@stuebingerb sure, happy to help. I'll check it out at the weekend, if you don't mind, because this is beefy PR 😄

Sure, take your time! This will likely stay open for a while anyway.

@grumpy-programmer
Copy link
Copy Markdown
Contributor

Really enjoyed going through this.

While reviewing, I also caught up on the changes from #560 - the separation between RequestError and ExecutionError is a great design decision. Having a clear boundary between "the client asked for something invalid" and "something went wrong during execution" makes the library much easier to reason about.

I particularly like the test case with ForbiddenError:

@Test
fun `resolvers should be able to throw custom request errors`() {
    data class Item(val data: String)
    class ForbiddenError(node: Execution.Node, message: String) :
        RequestError(message, node = node.selectionNode, extensions = mapOf("type" to "FORBIDDEN"))

    val schema = KGraphQL.schema {
        query("items") {
            resolver<Item, Execution.Node> { node: Execution.Node ->
                throw ForbiddenError(node, "Not allowed")
            }
        }
    }
    schema.executeBlocking("{ items { data }}") shouldBe """
        {"errors":[{"message":"Not allowed","locations":[{"line":1,"column":3}],"extensions":{"type":"FORBIDDEN"}}]}
    """.trimIndent()
}

That illustrates how flexible error handling has become, because the developer takes responsibility for how a request can fail: globally RequestError or partially ExecutionError with partial response.

Another nice flavor is Context.raiseError, good for handling more complex logic in resolver.

I appreciate the tremendous amount of work that went into this entire PR series with the error handling changes. Great job!

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c2bc6e17-3c82-42a7-a5ec-5ea04a18aa07

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7 issues found across 42 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="docs/content/Reference/errorHandling.md">

<violation number="1" location="docs/content/Reference/errorHandling.md:107">
P2: The docs incorrectly say the list entry would be “missing”; it should be `null` to match GraphQL semantics and the example response.</violation>
</file>

<file name="kgraphql/src/main/kotlin/com/apurebase/kgraphql/Context.kt">

<violation number="1" location="kgraphql/src/main/kotlin/com/apurebase/kgraphql/Context.kt:25">
P1: `Context.plus` shares the same mutable `errors` list, which can leak execution errors between derived contexts/requests.</violation>
</file>

<file name="kgraphql-ktor-stitched/src/main/kotlin/com/apurebase/kgraphql/stitched/schema/execution/AbstractRemoteRequestExecutor.kt">

<violation number="1" location="kgraphql-ktor-stitched/src/main/kotlin/com/apurebase/kgraphql/stitched/schema/execution/AbstractRemoteRequestExecutor.kt:72">
P2: Do not swallow coroutine cancellation here; rethrow `CancellationException` before converting failures to `ExecutionError`.</violation>
</file>

<file name="kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ArgumentTransformer.kt">

<violation number="1" location="kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ArgumentTransformer.kt:36">
P1: Unsupported argument coercion is now thrown as `ValidationException` (request error), which can incorrectly abort execution instead of returning a field-level execution error.</violation>
</file>

<file name="kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/scalar/Coercion.kt">

<violation number="1" location="kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/scalar/Coercion.kt:49">
P2: This catch path converts existing `ExecutionError`s into `InvalidInputValueException`, which changes error classification (e.g., `INTERNAL_SERVER_ERROR`/custom extensions become `BAD_USER_INPUT`). Preserve `ExecutionError` instead of re-wrapping it.</violation>
</file>

<file name="kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ParallelRequestExecutor.kt">

<violation number="1" location="kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ParallelRequestExecutor.kt:112">
P1: `data` nullability is derived from `resultMap` emptiness, so root-level non-null propagation can be serialized as partial object data instead of `data: null`.</violation>

<violation number="2" location="kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ParallelRequestExecutor.kt:308">
P2: Nested object field resolution no longer uses the configured coroutine dispatcher; it falls back to `Dispatchers.Default`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

inline fun <reified T : Any> get(): T? = get(T::class)

operator fun <T : Any> plus(value: T): Context = Context(map + (value::class to value))
operator fun <T : Any> plus(value: T): Context = Context(map + (value::class to value), errors)
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Context.plus shares the same mutable errors list, which can leak execution errors between derived contexts/requests.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At kgraphql/src/main/kotlin/com/apurebase/kgraphql/Context.kt, line 25:

<comment>`Context.plus` shares the same mutable `errors` list, which can leak execution errors between derived contexts/requests.</comment>

<file context>
@@ -18,5 +22,7 @@ class Context(private val map: Map<KClass<*>, Any>) {
     inline fun <reified T : Any> get(): T? = get(T::class)
 
-    operator fun <T : Any> plus(value: T): Context = Context(map + (value::class to value))
+    operator fun <T : Any> plus(value: T): Context = Context(map + (value::class to value), errors)
+
+    fun raiseError(error: ExecutionError) = errors.add(error)
</file context>
Suggested change
operator fun <T : Any> plus(value: T): Context = Context(map + (value::class to value), errors)
operator fun <T : Any> plus(value: T): Context = Context(map + (value::class to value))
Fix with Cubic

Comment on lines +36 to 39
throw ValidationException(
"'$funName' does support arguments: ${inputValues.map { it.name }}. Found arguments: ${args.keys}",
executionNode.selectionNode
)
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Unsupported argument coercion is now thrown as ValidationException (request error), which can incorrectly abort execution instead of returning a field-level execution error.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ArgumentTransformer.kt, line 36:

<comment>Unsupported argument coercion is now thrown as `ValidationException` (request error), which can incorrectly abort execution instead of returning a field-level execution error.</comment>

<file context>
@@ -31,7 +33,7 @@ open class ArgumentTransformer(val genericTypeResolver: GenericTypeResolver) {
 
         if (unsupportedArguments?.isNotEmpty() == true) {
-            throw InvalidInputValueException(
+            throw ValidationException(
                 "'$funName' does support arguments: ${inputValues.map { it.name }}. Found arguments: ${args.keys}",
                 executionNode.selectionNode
</file context>
Suggested change
throw ValidationException(
"'$funName' does support arguments: ${inputValues.map { it.name }}. Found arguments: ${args.keys}",
executionNode.selectionNode
)
throw InvalidInputValueException(
"'$funName' does support arguments: ${inputValues.map { it.name }}. Found arguments: ${args.keys}",
executionNode
)
Fix with Cubic

plan.associate { operation -> executeOperation(operation) }
}

val data = if (resultMap.values.any { it != null }) {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: data nullability is derived from resultMap emptiness, so root-level non-null propagation can be serialized as partial object data instead of data: null.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ParallelRequestExecutor.kt, line 112:

<comment>`data` nullability is derived from `resultMap` emptiness, so root-level non-null propagation can be serialized as partial object data instead of `data: null`.</comment>

<file context>
@@ -74,63 +74,92 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor {
+            plan.associate { operation -> executeOperation(operation) }
+        }
+
+        val data = if (resultMap.values.any { it != null }) {
+            jsonNodeFactory.objectNode()
+        } else {
</file context>
Fix with Cubic

```

=== "Response (HTTP 200)"
If the field `name` was declared as non-null, the whole list entry would be missing instead. However, the error itself would still be the same:
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The docs incorrectly say the list entry would be “missing”; it should be null to match GraphQL semantics and the example response.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/content/Reference/errorHandling.md, line 107:

<comment>The docs incorrectly say the list entry would be “missing”; it should be `null` to match GraphQL semantics and the example response.</comment>

<file context>
@@ -1,83 +1,179 @@
     ```
 
-=== "Response (HTTP 200)"
+If the field `name` was declared as non-null, the whole list entry would be missing instead. However, the error itself would still be the same:
+
+=== "SDL"
</file context>
Suggested change
If the field `name` was declared as non-null, the whole list entry would be missing instead. However, the error itself would still be the same:
If the field `name` was declared as non-null, the whole list entry would be `null` instead. However, the error itself would still be the same:
Fix with Cubic

Comment on lines +72 to 77
}.getOrElse {
ctx.raiseError(
ExecutionError(it.message ?: it.javaClass.simpleName, node, it, node.errorExtensions())
)
null
}
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Do not swallow coroutine cancellation here; rethrow CancellationException before converting failures to ExecutionError.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At kgraphql-ktor-stitched/src/main/kotlin/com/apurebase/kgraphql/stitched/schema/execution/AbstractRemoteRequestExecutor.kt, line 72:

<comment>Do not swallow coroutine cancellation here; rethrow `CancellationException` before converting failures to `ExecutionError`.</comment>

<file context>
@@ -34,59 +52,36 @@ abstract class AbstractRemoteRequestExecutor(private val objectMapper: ObjectMap
         }
-        return responseJson["data"]?.get(node.remoteOperation)
+        response.data?.get(node.remoteOperation)
+    }.getOrElse {
+        ctx.raiseError(
+            ExecutionError(it.message ?: it.javaClass.simpleName, node, it, node.errorExtensions())
</file context>
Suggested change
}.getOrElse {
ctx.raiseError(
ExecutionError(it.message ?: it.javaClass.simpleName, node, it, node.errorExtensions())
)
null
}
}.getOrElse {
if (it is kotlinx.coroutines.CancellationException) throw it
ctx.raiseError(
ExecutionError(it.message ?: it.javaClass.simpleName, node, it, node.errorExtensions())
)
null
}
Fix with Cubic

throw InvalidInputValueException(
message = "Cannot coerce '${value.valueNodeName}' to ${scalar.name}",
node = value,
message = (e as? GraphQLError)?.message ?: "Cannot coerce '${value.valueNodeName}' to ${scalar.name}",
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This catch path converts existing ExecutionErrors into InvalidInputValueException, which changes error classification (e.g., INTERNAL_SERVER_ERROR/custom extensions become BAD_USER_INPUT). Preserve ExecutionError instead of re-wrapping it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/scalar/Coercion.kt, line 49:

<comment>This catch path converts existing `ExecutionError`s into `InvalidInputValueException`, which changes error classification (e.g., `INTERNAL_SERVER_ERROR`/custom extensions become `BAD_USER_INPUT`). Preserve `ExecutionError` instead of re-wrapping it.</comment>

<file context>
@@ -39,17 +39,15 @@ fun <T : Any> deserializeScalar(scalar: Type.Scalar<T>, value: ValueNode): T {
         throw InvalidInputValueException(
-            message = "Cannot coerce '${value.valueNodeName}' to ${scalar.name}",
-            node = value,
+            message = (e as? GraphQLError)?.message ?: "Cannot coerce '${value.valueNodeName}' to ${scalar.name}",
+            node = executionNode,
             originalError = e
</file context>
Suggested change
message = (e as? GraphQLError)?.message ?: "Cannot coerce '${value.valueNodeName}' to ${scalar.name}",
message = when (e) {
is ExecutionError -> throw e
is GraphQLError -> e.message
else -> "Cannot coerce '${value.valueNodeName}' to ${scalar.name}"
},
Fix with Cubic

val objectNode = jsonNodeFactory.objectNode()
val deferreds = mutableListOf<Deferred<Map<String, Deferred<JsonNode?>?>>>()
for (child in node.children) {
val deferreds = node.children.mapIndexedParallel { _, child ->
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Nested object field resolution no longer uses the configured coroutine dispatcher; it falls back to Dispatchers.Default.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ParallelRequestExecutor.kt, line 308:

<comment>Nested object field resolution no longer uses the configured coroutine dispatcher; it falls back to `Dispatchers.Default`.</comment>

<file context>
@@ -248,35 +294,30 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor {
         val objectNode = jsonNodeFactory.objectNode()
-        val deferreds = mutableListOf<Deferred<Map<String, Deferred<JsonNode?>?>>>()
-        for (child in node.children) {
+        val deferreds = node.children.mapIndexedParallel { _, child ->
             when (child) {
-                is Execution.Fragment -> deferreds.add(ctx.scope.async {
</file context>
Suggested change
val deferreds = node.children.mapIndexedParallel { _, child ->
val deferreds = node.children.mapIndexedParallel(dispatcher) { _, child ->
Fix with Cubic

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/DefaultSchema.kt (1)

52-54: Consider catching JSON parsing exceptions.

If configuration.objectMapper.readTree(variables) throws (e.g., malformed JSON), it will propagate as a Jackson exception rather than a serialized GraphQL error. This could cause HTTP 500 responses instead of proper GraphQL error responses.

♻️ Proposed fix to handle malformed variable JSON
         try {
+            val parsedVariables = try {
+                variables
+                    ?.let { VariablesJson.Defined(configuration.objectMapper.readTree(variables)) }
+                    ?: VariablesJson.Empty()
+            } catch (e: Exception) {
+                throw ValidationException("Invalid JSON in variables: ${e.message}")
+            }
-            val parsedVariables = variables
-                ?.let { VariablesJson.Defined(configuration.objectMapper.readTree(variables)) }
-                ?: VariablesJson.Empty()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/DefaultSchema.kt`
around lines 52 - 54, Wrap the call to
configuration.objectMapper.readTree(variables) (used to build parsedVariables /
VariablesJson.Defined) in a try-catch for
com.fasterxml.jackson.core.JsonProcessingException (or general Jackson parsing
exceptions); on catch, do not let the exception propagate as a raw exception —
instead convert it into a GraphQL-friendly error path (for example return
VariablesJson.Empty() and/or throw a graphql error type that your execution
layer serializes, with a clear message like "Malformed variables JSON") so
malformed JSON yields a serialized GraphQL error rather than an HTTP 500.
docs/content/Reference/errorHandling.md (1)

13-13: Minor style suggestion: Consider "outside" instead of "outside of".

Static analysis flagged "outside of" as potentially redundant. The phrase "outside the schema" is slightly more concise.

-Additionally, every error can contain an entry with the key `extensions` that is a map of server-specific additions outside of the schema.
+Additionally, every error can contain an entry with the key `extensions` that is a map of server-specific additions outside the schema.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/content/Reference/errorHandling.md` at line 13, Update the sentence that
describes the `extensions` entry to use the more concise phrasing "outside the
schema" instead of "outside of the schema": locate the line discussing the
`extensions` map and replace "that is a map of server-specific additions outside
of the schema" with "that is a map of server-specific additions outside the
schema" so the documentation for `extensions` reads more succinctly while
preserving meaning.
kgraphql-ktor-stitched/src/main/kotlin/com/apurebase/kgraphql/stitched/schema/execution/AbstractRemoteRequestExecutor.kt (1)

36-36: Typo in comment: "reponse" should be "response".

-    // reponse location is not mapped because we want the local location anyway
+    // response location is not mapped because we want the local location anyway
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@kgraphql-ktor-stitched/src/main/kotlin/com/apurebase/kgraphql/stitched/schema/execution/AbstractRemoteRequestExecutor.kt`
at line 36, Typo in the comment inside class AbstractRemoteRequestExecutor:
change "reponse" to "response" in the inline comment ("// reponse location is
not mapped because we want the local location anyway") so the comment reads "//
response location is not mapped because we want the local location anyway".
kgraphql/src/testFixtures/kotlin/com/apurebase/kgraphql/CommonTestUtils.kt (1)

36-41: Consider extending errorType mapping for completeness.

The current mapping covers the main exception types, but ExecutionException and ExecutionError would both fall through to the else branch returning INTERNAL_SERVER_ERROR. This works correctly but could be made more explicit.

Also, if a caller uses expectRequestError<RequestError> (the base class), it would incorrectly expect INTERNAL_SERVER_ERROR instead of BAD_USER_INPUT.

Consider making base class handling explicit
 inline fun <reified T : GraphQLError> errorType() = when (T::class) {
     InvalidInputValueException::class -> BuiltInErrorCodes.BAD_USER_INPUT.name
     ValidationException::class -> BuiltInErrorCodes.GRAPHQL_VALIDATION_FAILED.name
     InvalidSyntaxException::class -> BuiltInErrorCodes.GRAPHQL_PARSE_FAILED.name
+    ExecutionException::class -> BuiltInErrorCodes.INTERNAL_SERVER_ERROR.name
+    // Base classes default to their typical error codes
+    RequestError::class -> BuiltInErrorCodes.BAD_USER_INPUT.name
     else -> BuiltInErrorCodes.INTERNAL_SERVER_ERROR.name
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@kgraphql/src/testFixtures/kotlin/com/apurebase/kgraphql/CommonTestUtils.kt`
around lines 36 - 41, The when-mapping in errorType() misses explicit branches
for RequestError, ExecutionException and ExecutionError so base-class callers
fall through to INTERNAL_SERVER_ERROR; update the when in errorType() to add
cases: RequestError::class -> BuiltInErrorCodes.BAD_USER_INPUT.name,
ExecutionException::class -> BuiltInErrorCodes.INTERNAL_SERVER_ERROR.name, and
ExecutionError::class -> BuiltInErrorCodes.INTERNAL_SERVER_ERROR.name (keeping
existing mappings for InvalidInputValueException, ValidationException,
InvalidSyntaxException) so behavior is explicit and callers using RequestError
get BAD_USER_INPUT.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/EnumsSpecificationTest.kt`:
- Around line 36-38: The test currently asserts a runtime execution error by
using expectExecutionError<InvalidInputValueException> for passing a string
literal to an enum input; instead, update the assertion to expect a
request/validation-phase error (e.g., replace
expectExecutionError<InvalidInputValueException> with
expectRequestError<InvalidInputValueException>) so the failing call to
schema.executeBlocking("{cool(cool : \"COOL\")}") is validated as a request
error; locate and update the assertion that references expectExecutionError and
InvalidInputValueException in EnumsSpecificationTest.kt to the request-phase
assertion variant.

In
`@kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/ListsSpecificationTest.kt`:
- Around line 65-67: The test currently asserts a variable coercion failure as
an execution error; update the expectation to a request error by replacing
expectExecutionError<InvalidInputValueException> with
expectRequestError<InvalidInputValueException> for the case that runs
schema.executeBlocking("query(\$list: [String!]!) { list(list: \$list) }",
variables) so the null-in-list variable coercion is treated as a request
validation error rather than an execution error.

In
`@kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/ScalarsSpecificationTest.kt`:
- Around line 247-269: Tests in ScalarsSpecificationTest.kt assert
InvalidInputValueException for ID coercion but ID_COERCION in BuiltInScalars.kt
currently throws ValidationException; update the implementation to throw
InvalidInputValueException instead (or, if you prefer to change the tests,
update the expectExecutionError calls to expect ValidationException) so both
sides use the same exception type; modify the ID_COERCION handler in
BuiltInScalars.kt to construct/throw InvalidInputValueException with the same
message format used elsewhere to keep test messages consistent.
- Around line 148-150: The test fails because BuiltInScalars.kt currently throws
ValidationException for Int overflow but the test
(expectExecutionError<InvalidInputValueException>) expects an
InvalidInputValueException (BAD_USER_INPUT); update the scalar coercion path
that handles integer parsing/overflow (the Int scalar coercion function in
BuiltInScalars.kt — e.g., the coerce/parse branch that currently throws
ValidationException) to throw InvalidInputValueException with the same error
message so errors[index].extensions.type becomes BAD_USER_INPUT and the existing
expectExecutionError<InvalidInputValueException> assertion passes.

---

Nitpick comments:
In `@docs/content/Reference/errorHandling.md`:
- Line 13: Update the sentence that describes the `extensions` entry to use the
more concise phrasing "outside the schema" instead of "outside of the schema":
locate the line discussing the `extensions` map and replace "that is a map of
server-specific additions outside of the schema" with "that is a map of
server-specific additions outside the schema" so the documentation for
`extensions` reads more succinctly while preserving meaning.

In
`@kgraphql-ktor-stitched/src/main/kotlin/com/apurebase/kgraphql/stitched/schema/execution/AbstractRemoteRequestExecutor.kt`:
- Line 36: Typo in the comment inside class AbstractRemoteRequestExecutor:
change "reponse" to "response" in the inline comment ("// reponse location is
not mapped because we want the local location anyway") so the comment reads "//
response location is not mapped because we want the local location anyway".

In `@kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/DefaultSchema.kt`:
- Around line 52-54: Wrap the call to
configuration.objectMapper.readTree(variables) (used to build parsedVariables /
VariablesJson.Defined) in a try-catch for
com.fasterxml.jackson.core.JsonProcessingException (or general Jackson parsing
exceptions); on catch, do not let the exception propagate as a raw exception —
instead convert it into a GraphQL-friendly error path (for example return
VariablesJson.Empty() and/or throw a graphql error type that your execution
layer serializes, with a clear message like "Malformed variables JSON") so
malformed JSON yields a serialized GraphQL error rather than an HTTP 500.

In `@kgraphql/src/testFixtures/kotlin/com/apurebase/kgraphql/CommonTestUtils.kt`:
- Around line 36-41: The when-mapping in errorType() misses explicit branches
for RequestError, ExecutionException and ExecutionError so base-class callers
fall through to INTERNAL_SERVER_ERROR; update the when in errorType() to add
cases: RequestError::class -> BuiltInErrorCodes.BAD_USER_INPUT.name,
ExecutionException::class -> BuiltInErrorCodes.INTERNAL_SERVER_ERROR.name, and
ExecutionError::class -> BuiltInErrorCodes.INTERNAL_SERVER_ERROR.name (keeping
existing mappings for InvalidInputValueException, ValidationException,
InvalidSyntaxException) so behavior is explicit and callers using RequestError
get BAD_USER_INPUT.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cfd51f98-dcc0-484d-b73b-ff96f92ed2f7

📥 Commits

Reviewing files that changed from the base of the PR and between e6969de and c70d308.

📒 Files selected for processing (42)
  • docs/content/Reference/errorHandling.md
  • gradle/libs.versions.toml
  • kgraphql-ktor-stitched/api/kgraphql-ktor-stitched.api
  • kgraphql-ktor-stitched/src/main/kotlin/com/apurebase/kgraphql/stitched/RemoteExecutionException.kt
  • kgraphql-ktor-stitched/src/main/kotlin/com/apurebase/kgraphql/stitched/schema/execution/AbstractRemoteRequestExecutor.kt
  • kgraphql-ktor-stitched/src/test/kotlin/com/apurebase/kgraphql/stitched/schema/execution/StitchedSchemaExecutionTest.kt
  • kgraphql-ktor/src/test/kotlin/com/apurebase/kgraphql/KtorFeatureTest.kt
  • kgraphql/api/kgraphql.api
  • kgraphql/build.gradle.kts
  • kgraphql/src/main/kotlin/com/apurebase/kgraphql/Context.kt
  • kgraphql/src/main/kotlin/com/apurebase/kgraphql/GraphQLError.kt
  • kgraphql/src/main/kotlin/com/apurebase/kgraphql/helpers/KGraphQLExtensions.kt
  • kgraphql/src/main/kotlin/com/apurebase/kgraphql/request/Variables.kt
  • kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/DefaultSchema.kt
  • kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/builtin/BuiltInScalars.kt
  • kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ArgumentTransformer.kt
  • kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ParallelRequestExecutor.kt
  • kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/scalar/Coercion.kt
  • kgraphql/src/test/kotlin/com/apurebase/kgraphql/access/AccessRulesTest.kt
  • kgraphql/src/test/kotlin/com/apurebase/kgraphql/configuration/SchemaConfigurationTest.kt
  • kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/MutationTest.kt
  • kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/ParallelExecutionTest.kt
  • kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/QueryTest.kt
  • kgraphql/src/test/kotlin/com/apurebase/kgraphql/schema/SchemaBuilderTest.kt
  • kgraphql/src/test/kotlin/com/apurebase/kgraphql/schema/SchemaInheritanceTest.kt
  • kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/IntrospectionSpecificationTest.kt
  • kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/FragmentsSpecificationTest.kt
  • kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/InputValuesSpecificationTest.kt
  • kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/ListInputCoercionTest.kt
  • kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/OperationsSpecificationTest.kt
  • kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/QueryDocumentSpecificationTest.kt
  • kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/SourceTextSpecificationTest.kt
  • kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/VariablesSpecificationTest.kt
  • kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/DirectivesSpecificationTest.kt
  • kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/EnumsSpecificationTest.kt
  • kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/InputObjectsSpecificationTest.kt
  • kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/InterfacesSpecificationTest.kt
  • kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/ListsSpecificationTest.kt
  • kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/NonNullSpecificationTest.kt
  • kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/ScalarsSpecificationTest.kt
  • kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/UnionsSpecificationTest.kt
  • kgraphql/src/testFixtures/kotlin/com/apurebase/kgraphql/CommonTestUtils.kt
💤 Files with no reviewable changes (2)
  • kgraphql-ktor-stitched/src/main/kotlin/com/apurebase/kgraphql/stitched/RemoteExecutionException.kt
  • kgraphql-ktor-stitched/api/kgraphql-ktor-stitched.api

Comment on lines +36 to 38
expectExecutionError<InvalidInputValueException>("Cannot coerce string literal '\"COOL\"' to enum Coolness") {
schema.executeBlocking("{cool(cool : \"COOL\")}")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Enum literal type mismatch should be asserted as request error.

At Line 36, cool: "COOL" (string literal) for enum input is a validation/request-phase error, not an execution-phase field error. This expectation currently cements non-spec behavior.

Suggested test expectation update
-import com.apurebase.kgraphql.InvalidInputValueException
+import com.apurebase.kgraphql.ValidationException
 import com.apurebase.kgraphql.expect
-import com.apurebase.kgraphql.expectExecutionError
+import com.apurebase.kgraphql.expectRequestError
@@
-        expectExecutionError<InvalidInputValueException>("Cannot coerce string literal '\"COOL\"' to enum Coolness") {
+        expectRequestError<ValidationException>("Cannot coerce string literal '\"COOL\"' to enum Coolness") {
             schema.executeBlocking("{cool(cool : \"COOL\")}")
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/EnumsSpecificationTest.kt`
around lines 36 - 38, The test currently asserts a runtime execution error by
using expectExecutionError<InvalidInputValueException> for passing a string
literal to an enum input; instead, update the assertion to expect a
request/validation-phase error (e.g., replace
expectExecutionError<InvalidInputValueException> with
expectRequestError<InvalidInputValueException>) so the failing call to
schema.executeBlocking("{cool(cool : \"COOL\")}") is validated as a request
error; locate and update the assertion that references expectExecutionError and
InvalidInputValueException in EnumsSpecificationTest.kt to the request-phase
assertion variant.

Comment on lines +65 to 67
expectExecutionError<InvalidInputValueException>("Cannot coerce 'null' to String") {
schema.executeBlocking("query(\$list: [String!]!) { list(list: \$list) }", variables)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

This test locks in the wrong error category for variable coercion.

At Line 65, invalid variable value coercion (null inside [String!]!) should be asserted as a request error, not an execution error. Keeping it as execution error conflicts with spec-aligned request/execution separation and can mask the real engine bug.

Suggested test expectation update
-import com.apurebase.kgraphql.InvalidInputValueException
-import com.apurebase.kgraphql.expectExecutionError
+import com.apurebase.kgraphql.ValidationException
+import com.apurebase.kgraphql.expectRequestError
@@
-        expectExecutionError<InvalidInputValueException>("Cannot coerce 'null' to String") {
+        expectRequestError<ValidationException>("Cannot coerce 'null' to String") {
             schema.executeBlocking("query(\$list: [String!]!) { list(list: \$list) }", variables)
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/ListsSpecificationTest.kt`
around lines 65 - 67, The test currently asserts a variable coercion failure as
an execution error; update the expectation to a request error by replacing
expectExecutionError<InvalidInputValueException> with
expectRequestError<InvalidInputValueException> for the case that runs
schema.executeBlocking("query(\$list: [String!]!) { list(list: \$list) }",
variables) so the null-in-list variable coercion is treated as a request
validation error rather than an execution error.

Comment on lines +148 to 150
expectExecutionError<InvalidInputValueException>("Cannot coerce '${Integer.MAX_VALUE.toLong() + 2L}' to Int as it is greater than (2^-31)-1") {
schema.executeBlocking("mutation { Int(int: ${Integer.MAX_VALUE.toLong() + 2L}) }")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify what exception type INT_COERCION throws
rg -n "throw.*Exception" kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/builtin/BuiltInScalars.kt

# Check if InvalidInputValueException extends ExecutionError
rg -n "class InvalidInputValueException" --type=kt -A 3

Repository: stuebingerb/KGraphQL

Length of output: 1638


🏁 Script executed:

rg -n "class ValidationException" --type-list --type python --type go | head -20

Repository: stuebingerb/KGraphQL

Length of output: 453


🏁 Script executed:

rg -n "class ValidationException" -A 3

Repository: stuebingerb/KGraphQL

Length of output: 540


🏁 Script executed:

rg -n "class InvalidInputValueException" -A 3

Repository: stuebingerb/KGraphQL

Length of output: 529


🏁 Script executed:

rg -n "fun expectExecutionError" -A 10

Repository: stuebingerb/KGraphQL

Length of output: 46


🏁 Script executed:

rg -n "expectExecutionError" kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/ScalarsSpecificationTest.kt | head -5

Repository: stuebingerb/KGraphQL

Length of output: 560


🏁 Script executed:

rg -n "fun expectExecutionError" -B 2 -A 15

Repository: stuebingerb/KGraphQL

Length of output: 46


🏁 Script executed:

rg -n "class RequestError" -A 5

Repository: stuebingerb/KGraphQL

Length of output: 644


🏁 Script executed:

rg -n "class ExecutionError" -A 5

Repository: stuebingerb/KGraphQL

Length of output: 695


🏁 Script executed:

find . -name "*.kt" -type f | xargs grep -l "fun expectExecutionError" 2>/dev/null

Repository: stuebingerb/KGraphQL

Length of output: 48


🏁 Script executed:

rg -n "inline fun expectExecutionError\|fun expectExecutionError" -B 2 -A 20

Repository: stuebingerb/KGraphQL

Length of output: 46


🏁 Script executed:

rg -n "expectExecutionError" -l

Repository: stuebingerb/KGraphQL

Length of output: 1371


🏁 Script executed:

fd "\.kt$" -type f | head -20

Repository: stuebingerb/KGraphQL

Length of output: 234


🏁 Script executed:

cat kgraphql/src/testFixtures/kotlin/com/apurebase/kgraphql/CommonTestUtils.kt

Repository: stuebingerb/KGraphQL

Length of output: 1981


Test will fail due to exception type mismatch.

The test expects InvalidInputValueException (error type BAD_USER_INPUT), but BuiltInScalars.kt throws ValidationException (error type GRAPHQL_VALIDATION_FAILED). The expectExecutionError helper checks that errors[index].extensions.type matches the expected error type. Since the thrown exception type differs from what the test asserts, the error type check will fail.

Either update the test to expect expectRequestError<ValidationException>() or change the implementation to throw InvalidInputValueException.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/ScalarsSpecificationTest.kt`
around lines 148 - 150, The test fails because BuiltInScalars.kt currently
throws ValidationException for Int overflow but the test
(expectExecutionError<InvalidInputValueException>) expects an
InvalidInputValueException (BAD_USER_INPUT); update the scalar coercion path
that handles integer parsing/overflow (the Int scalar coercion function in
BuiltInScalars.kt — e.g., the coerce/parse branch that currently throws
ValidationException) to throw InvalidInputValueException with the same error
message so errors[index].extensions.type becomes BAD_USER_INPUT and the existing
expectExecutionError<InvalidInputValueException> assertion passes.

Comment on lines +247 to 269
expectExecutionError<InvalidInputValueException>("Cannot coerce '4.0' to ID") {
testedSchema.executeBlocking("query(\$id: ID! = 4.0) { personById(id: \$id) { id, name } }")
}

// Boolean (should fail)
expect<InvalidInputValueException>("Cannot coerce 'true' to ID") {
expectExecutionError<InvalidInputValueException>("Cannot coerce 'true' to ID") {
testedSchema.executeBlocking("query(\$id: ID! = true) { personById(id: \$id) { id, name } }")
}

// List of strings (should fail)
expect<InvalidInputValueException>("Cannot coerce '[\"4\", \"5\"]' to ID") {
expectExecutionError<InvalidInputValueException>("Cannot coerce '[\"4\", \"5\"]' to ID") {
testedSchema.executeBlocking("query(\$id: ID! = [\"4\", \"5\"]) { personById(id: \$id) { id, name } }")
}

// Object (should fail)
expect<InvalidInputValueException>("Property 'value' on 'ID' does not exist") {
expectExecutionError<InvalidInputValueException>("Property 'value' on 'ID' does not exist") {
testedSchema.executeBlocking("query(\$id: ID! = {value: \"4\"}) { personById(id: \$id) { id, name } }")
}

// Null (should fail)
expect<InvalidInputValueException>("Cannot coerce 'null' to ID") {
expectExecutionError<InvalidInputValueException>("Cannot coerce 'null' to ID") {
testedSchema.executeBlocking("query(\$id: ID! = null) { personById(id: \$id) { id, name } }")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Same inconsistency applies to ID coercion tests.

These tests also expect InvalidInputValueException but ID_COERCION in BuiltInScalars.kt (line 178) throws ValidationException.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/ScalarsSpecificationTest.kt`
around lines 247 - 269, Tests in ScalarsSpecificationTest.kt assert
InvalidInputValueException for ID coercion but ID_COERCION in BuiltInScalars.kt
currently throws ValidationException; update the implementation to throw
InvalidInputValueException instead (or, if you prefer to change the tests,
update the expectExecutionError calls to expect ValidationException) so both
sides use the same exception type; modify the ID_COERCION handler in
BuiltInScalars.kt to construct/throw InvalidInputValueException with the same
message format used elsewhere to keep test messages consistent.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 42 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/builtin/BuiltInScalars.kt">

<violation number="1" location="kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/builtin/BuiltInScalars.kt:164">
P2: Include `valueNode` when throwing this `ValidationException` to preserve GraphQL error locations.</violation>
</file>

<file name="kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/VariablesSpecificationTest.kt">

<violation number="1" location="kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/VariablesSpecificationTest.kt:49">
P2: Variable coercion failures are request errors, so these cases should not be asserted as execution errors.</violation>
</file>

<file name="kgraphql/src/main/kotlin/com/apurebase/kgraphql/helpers/KGraphQLExtensions.kt">

<violation number="1" location="kgraphql/src/main/kotlin/com/apurebase/kgraphql/helpers/KGraphQLExtensions.kt:145">
P2: `extensions` are serialized with `valueToTree`, which can fail for arbitrary `Map<String, Any?>` values and break error response serialization. Use the existing defensive conversion path so unknown extension value types are stringified instead of throwing.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Client
    participant Schema as DefaultSchema
    participant Parser as Request Interpreter
    participant Executor as ParallelRequestExecutor
    participant Context as Execution Context
    participant Resolver as Field Resolver / Data Loader
    participant Remote as Remote Request Executor

    Note over Client,Remote: KGraphQL Runtime Request Flow (Sept 2025 Spec Support)

    Client->>Schema: execute(request, variables, context)
    
    Schema->>Parser: parseDocument & createExecutionPlan
    
    alt Request Error (Parsing/Validation/Introspection)
        Parser-->>Schema: throw RequestError (e.g., ValidationException)
        Schema->>Schema: CHANGED: serialize error without data key
        Schema-->>Client: { "errors": [...] }
    else Valid Request
        Parser-->>Schema: ExecutionPlan
        
        Schema->>Executor: suspendExecute(plan, variables, context)
        
        Executor->>Context: NEW: Initialize shared thread-safe error list
        
        loop For each field in ExecutionPlan
            Executor->>Context: Check access rules / directives
            
            alt Local Resolver
                Executor->>Resolver: invoke()
                alt Success
                    Resolver-->>Executor: data
                else Execution Error (Caught)
                    Resolver-->>Executor: throw Exception / NEW: ctx.raiseError()
                    Executor->>Context: NEW: Add ExecutionError with path & location
                    Executor->>Executor: CHANGED: Apply nullability rules (null field or bubble up)
                end
            else Remote Resolver (Stitched)
                Executor->>Remote: execute(node, ctx)
                Remote->>Remote: Fetch external GraphQL
                opt Remote Errors Present
                    Remote->>Context: NEW: Map and raise errors with adjusted paths
                end
                Remote-->>Executor: partial data
            end
        end

        Executor->>Executor: NEW: Finalize result tree (handling null-bubbling)
        
        Executor->>Executor: NEW: Serialize JSON (errors key FIRST, then data)
        Executor-->>Schema: JSON Result String
        Schema-->>Client: { "errors": [...], "data": {...} }
    end
Loading

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.

1L -> true

else -> throw IllegalArgumentException("Cannot coerce '${valueNode.value}' to Boolean")
else -> throw ValidationException("Cannot coerce '${valueNode.value}' to Boolean")
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Include valueNode when throwing this ValidationException to preserve GraphQL error locations.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/builtin/BuiltInScalars.kt, line 164:

<comment>Include `valueNode` when throwing this `ValidationException` to preserve GraphQL error locations.</comment>

<file context>
@@ -154,17 +154,17 @@ object BOOLEAN_COERCION : StringScalarCoercion<Boolean> {
             1L -> true
 
-            else -> throw IllegalArgumentException("Cannot coerce '${valueNode.value}' to Boolean")
+            else -> throw ValidationException("Cannot coerce '${valueNode.value}' to Boolean")
         }
 
</file context>
Suggested change
else -> throw ValidationException("Cannot coerce '${valueNode.value}' to Boolean")
else -> throw ValidationException("Cannot coerce '${valueNode.value}' to Boolean", valueNode)
Fix with Cubic

@Test
fun `query with int variable should not allow floating point numbers that are not whole`() {
expect<InvalidInputValueException>("Cannot coerce '1.01' to Int") {
expectExecutionError<InvalidInputValueException>("Cannot coerce '1.01' to Int") {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Variable coercion failures are request errors, so these cases should not be asserted as execution errors.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/VariablesSpecificationTest.kt, line 49:

<comment>Variable coercion failures are request errors, so these cases should not be asserted as execution errors.</comment>

<file context>
@@ -45,7 +46,7 @@ class VariablesSpecificationTest : BaseSchemaTest() {
     @Test
     fun `query with int variable should not allow floating point numbers that are not whole`() {
-        expect<InvalidInputValueException>("Cannot coerce '1.01' to Int") {
+        expectExecutionError<InvalidInputValueException>("Cannot coerce '1.01' to Int") {
             testedSchema.executeBlocking(
                 request = "query(\$rank: Int!) {filmByRank(rank: \$rank) {title}}",
</file context>
Fix with Cubic

}
set<JsonNode>("path", objectMapper.valueToTree(error.path))
error.extensions?.let {
set<JsonNode>("extensions", objectMapper.valueToTree(it))
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: extensions are serialized with valueToTree, which can fail for arbitrary Map<String, Any?> values and break error response serialization. Use the existing defensive conversion path so unknown extension value types are stringified instead of throwing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At kgraphql/src/main/kotlin/com/apurebase/kgraphql/helpers/KGraphQLExtensions.kt, line 145:

<comment>`extensions` are serialized with `valueToTree`, which can fail for arbitrary `Map<String, Any?>` values and break error response serialization. Use the existing defensive conversion path so unknown extension value types are stringified instead of throwing.</comment>

<file context>
@@ -129,3 +130,21 @@ fun JsonNode?.toValueNode(expectedType: __Type): ValueNode = when (this) {
+                    }
+                    set<JsonNode>("path", objectMapper.valueToTree(error.path))
+                    error.extensions?.let {
+                        set<JsonNode>("extensions", objectMapper.valueToTree(it))
+                    }
+                }
</file context>
Suggested change
set<JsonNode>("extensions", objectMapper.valueToTree(it))
set<JsonNode>("extensions", objectMapper.readTree(it.toJsonElement().toString()))
Fix with Cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support partial responses

2 participants