diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/ContextDataFetcherDecorator.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/ContextDataFetcherDecorator.java index 0ef5d41f..f680738f 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/ContextDataFetcherDecorator.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/ContextDataFetcherDecorator.java @@ -21,6 +21,7 @@ import java.util.List; import graphql.ExecutionInput; import graphql.GraphQLContext; import graphql.TrivialDataFetcher; +import graphql.execution.AbortExecutionException; import graphql.execution.DataFetcherResult; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; @@ -107,6 +108,11 @@ final class ContextDataFetcherDecorator implements DataFetcher { if (value == null) { return null; } + if (ContextPropagationHelper.isCancelled(graphQlContext)) { + return DataFetcherResult.newResult() + .error(new AbortExecutionException("GraphQL request has been cancelled by the client.")) + .build(); + } if (this.subscription) { Flux subscriptionResult = ReactiveAdapterRegistryHelper.toSubscriptionFlux(value) diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/ContextPropagationHelper.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/ContextPropagationHelper.java index 11cf589b..7a45d7ff 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/ContextPropagationHelper.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/ContextPropagationHelper.java @@ -131,6 +131,21 @@ public abstract class ContextPropagationHelper { return requestCancelled; } + /** + * Return {@code true} if the current request has been cancelled, {@code false} otherwise. + * This checks whether a {@link #createCancelPublisher(GraphQLContext) cancellation publisher is present} + * in the given context and the cancel signal has fired already. + * @param context the current GraphQL context + * @since 1.4.0 + */ + public static boolean isCancelled(GraphQLContext context) { + Mono cancelSignal = context.get(CANCEL_PUBLISHER_KEY); + if (cancelSignal != null) { + return cancelSignal.toFuture().isDone(); + } + return false; + } + /** * Bind the source {@link Mono} to the publisher from the given {@link GraphQLContext}. * The returned {@code Mono} will be cancelled when this publisher completes. diff --git a/spring-graphql/src/test/java/org/springframework/graphql/execution/ContextDataFetcherDecoratorTests.java b/spring-graphql/src/test/java/org/springframework/graphql/execution/ContextDataFetcherDecoratorTests.java index ff1ccfc6..e41ab704 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/execution/ContextDataFetcherDecoratorTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/execution/ContextDataFetcherDecoratorTests.java @@ -29,6 +29,7 @@ import graphql.GraphQL; import graphql.GraphQLError; import graphql.GraphqlErrorBuilder; import graphql.TrivialDataFetcher; +import graphql.execution.AbortExecutionException; import graphql.execution.DataFetcherResult; import graphql.schema.DataFetcher; import graphql.schema.DataFetcherFactories; @@ -55,7 +56,9 @@ import static org.awaitility.Awaitility.await; /** * Tests for {@link ContextDataFetcherDecorator}. + * * @author Rossen Stoyanchev + * @author Brian Clozel */ @SuppressWarnings("ReactiveStreamsUnusedPublisher") public class ContextDataFetcherDecoratorTests { @@ -288,7 +291,7 @@ public class ContextDataFetcherDecoratorTests { } @Test - void cancelMonoDataFetcherWhenRequestCancelled() throws Exception { + void cancelMonoDataFetcherWhenRequestCancelled() { AtomicBoolean dataFetcherCancelled = new AtomicBoolean(); GraphQL graphQl = GraphQlSetup.schemaContent(SCHEMA_CONTENT) .queryFetcher("greeting", (env) -> @@ -307,7 +310,7 @@ public class ContextDataFetcherDecoratorTests { } @Test - void cancelFluxDataFetcherWhenRequestCancelled() throws Exception { + void cancelFluxDataFetcherWhenRequestCancelled() { AtomicBoolean dataFetcherCancelled = new AtomicBoolean(); GraphQL graphQl = GraphQlSetup.schemaContent(SCHEMA_CONTENT) .queryFetcher("greeting", (env) -> @@ -325,6 +328,22 @@ public class ContextDataFetcherDecoratorTests { await().atMost(Duration.ofSeconds(2)).until(dataFetcherCancelled::get); } + @Test + void returnAbortExecutionForBlockingDataFetcherWhenRequestCancelled() throws Exception { + GraphQL graphQl = GraphQlSetup.schemaContent(SCHEMA_CONTENT) + .queryFetcher("greeting", (env) -> "Hello") + .toGraphQl(); + + ExecutionInput input = ExecutionInput.newExecutionInput().query("{ greeting }").build(); + Sinks.Empty requestCancelled = ContextPropagationHelper.createCancelPublisher(input.getGraphQLContext()); + requestCancelled.tryEmitEmpty(); + ExecutionResult result = graphQl.executeAsync(input).get(); + + assertThat(result.getErrors()).hasSize(1); + assertThat(result.getErrors().get(0)).isInstanceOf(AbortExecutionException.class) + .extracting("message").asString().isEqualTo("GraphQL request has been cancelled by the client."); + } + @Test void cancelFluxDataFetcherSubscriptionWhenRequestCancelled() throws Exception { AtomicBoolean dataFetcherCancelled = new AtomicBoolean();