From 835e9db34c6beeabe64c011cefb41ca9dea36b75 Mon Sep 17 00:00:00 2001 From: Antoine Boyer Date: Tue, 13 Jan 2026 15:57:47 -0800 Subject: [PATCH] Add FieldFetchingInstrumentationContext.onExceptionHandled call to ChainedInstrumentation Missed it from PR #4206 --- .../ChainedInstrumentation.java | 6 ++ .../ChainedInstrumentationStateTest.groovy | 74 +++++++++++++++++++ ...FieldFetchingInstrumentationContext.groovy | 7 ++ .../TestingInstrumentContext.groovy | 2 - 4 files changed, 87 insertions(+), 2 deletions(-) diff --git a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java index 2779a10a01..4ae65742bf 100644 --- a/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java +++ b/src/main/java/graphql/execution/instrumentation/ChainedInstrumentation.java @@ -6,6 +6,7 @@ import graphql.ExperimentalApi; import graphql.PublicApi; import graphql.execution.Async; +import graphql.execution.DataFetcherResult; import graphql.execution.ExecutionContext; import graphql.execution.FieldValueInfo; import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters; @@ -380,6 +381,11 @@ public void onFetchedValue(Object fetchedValue) { contexts.forEach(context -> context.onFetchedValue(fetchedValue)); } + @Override + public void onExceptionHandled(DataFetcherResult dataFetcherResult) { + contexts.forEach(context -> context.onExceptionHandled(dataFetcherResult)); + } + @Override public void onCompleted(Object result, Throwable t) { contexts.forEach(context -> context.onCompleted(result, t)); diff --git a/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy b/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy index 88d7c86538..a0c67d6848 100644 --- a/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/ChainedInstrumentationStateTest.groovy @@ -8,6 +8,8 @@ import graphql.execution.AsyncExecutionStrategy import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters import graphql.execution.instrumentation.parameters.InstrumentationValidationParameters +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment import graphql.validation.ValidationError import spock.lang.Specification @@ -227,6 +229,78 @@ class ChainedInstrumentationStateTest extends Specification { assertCalls(c) } + def "basic chaining and state management when exception raised in data fetching"() { + + def a = new NamedInstrumentation("A") + def b = new NamedInstrumentation("B") + def c = new NamedInstrumentation("C") + def nullState = new SimplePerformantInstrumentation() + + def chainedInstrumentation = new ChainedInstrumentation([ + a, + b, + nullState, + c, + ]) + + def query = """ + query HeroNameAndFriendsQuery { + hero { + id + } + } + """ + + def expected = "onExceptionHandled:fetch-id" + + + when: + def strategy = new AsyncExecutionStrategy() + def schema = StarWarsSchema.starWarsSchema + def graphQL = GraphQL + .newGraphQL(schema.transform { schemaBuilder -> + // throw exception when fetching the hero id + def exceptionDataFetcher = new DataFetcher() { + @Override + Object get(DataFetchingEnvironment environment) { + throw new RuntimeException("Data fetcher exception") + } + } + schemaBuilder.codeRegistry(schema.codeRegistry.transform { + it.dataFetcher( + schema.getObjectType("Human"), + schema.getObjectType("Human").getFieldDefinition("id"), + exceptionDataFetcher + ) + it.dataFetcher( + schema.getObjectType("Droid"), + schema.getObjectType("Droid").getFieldDefinition("id"), + exceptionDataFetcher + ) + return it + } + ) + }) + .queryExecutionStrategy(strategy) + .instrumentation(chainedInstrumentation) + .build() + + graphQL.execute(query) + + then: + + chainedInstrumentation.getInstrumentations().size() == 4 + + a.executionList.any { it == expected } + b.executionList.any { it == expected } + c.executionList.any { it == expected } + + assertCalls(a) + assertCalls(b) + assertCalls(c) + + } + def "empty chain"() { def chainedInstrumentation = new ChainedInstrumentation(Arrays.asList()) diff --git a/src/test/groovy/graphql/execution/instrumentation/TestingFieldFetchingInstrumentationContext.groovy b/src/test/groovy/graphql/execution/instrumentation/TestingFieldFetchingInstrumentationContext.groovy index 50fcaccd2e..c9e19ba631 100644 --- a/src/test/groovy/graphql/execution/instrumentation/TestingFieldFetchingInstrumentationContext.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/TestingFieldFetchingInstrumentationContext.groovy @@ -1,9 +1,16 @@ package graphql.execution.instrumentation +import graphql.execution.DataFetcherResult + class TestingFieldFetchingInstrumentationContext extends TestingInstrumentContext implements FieldFetchingInstrumentationContext { TestingFieldFetchingInstrumentationContext(Object op, Object executionList, Object throwableList, Boolean useOnDispatch) { super(op, executionList, throwableList, useOnDispatch) } + + @Override + void onExceptionHandled(DataFetcherResult dataFetcherResult) { + executionList << "onExceptionHandled:$op" + } } diff --git a/src/test/groovy/graphql/execution/instrumentation/TestingInstrumentContext.groovy b/src/test/groovy/graphql/execution/instrumentation/TestingInstrumentContext.groovy index 402fd2aee0..3e627b3684 100644 --- a/src/test/groovy/graphql/execution/instrumentation/TestingInstrumentContext.groovy +++ b/src/test/groovy/graphql/execution/instrumentation/TestingInstrumentContext.groovy @@ -1,7 +1,5 @@ package graphql.execution.instrumentation -import java.util.concurrent.CompletableFuture - class TestingInstrumentContext implements InstrumentationContext { def op def start = System.currentTimeMillis()