Skip to content

Commit 33bdea9

Browse files
committed
Consistent catch handling in InvocableHandlerMethodSupport
Closes gh-973
1 parent 91b6beb commit 33bdea9

File tree

2 files changed

+79
-33
lines changed

2 files changed

+79
-33
lines changed

spring-graphql/src/main/java/org/springframework/graphql/data/method/InvocableHandlerMethodSupport.java

+44-27
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -87,23 +87,13 @@ protected Object doInvoke(GraphQLContext graphQLContext, Object... argValues) {
8787
return invokeSuspendingFunction(getBean(), method, argValues);
8888
}
8989
Object result = method.invoke(getBean(), argValues);
90-
return handleReturnValue(graphQLContext, result);
90+
return handleReturnValue(graphQLContext, result, method, argValues);
9191
}
9292
catch (IllegalArgumentException ex) {
93-
assertTargetBean(method, getBean(), argValues);
94-
String text = (ex.getMessage() != null) ? ex.getMessage() : "Illegal argument";
95-
return Mono.error(new IllegalStateException(formatInvokeError(text, argValues), ex));
93+
return Mono.error(processIllegalArgumentException(argValues, ex, method));
9694
}
9795
catch (InvocationTargetException ex) {
98-
// Unwrap for DataFetcherExceptionResolvers ...
99-
Throwable targetException = ex.getTargetException();
100-
if (targetException instanceof Error || targetException instanceof Exception) {
101-
return Mono.error(targetException);
102-
}
103-
else {
104-
return Mono.error(new IllegalStateException(
105-
formatInvokeError("Invocation failure", argValues), targetException));
106-
}
96+
return Mono.error(processInvocationTargetException(argValues, ex));
10797
}
10898
catch (Throwable ex) {
10999
return Mono.error(ex);
@@ -124,24 +114,51 @@ private static Object invokeSuspendingFunction(Object bean, Method method, Objec
124114
}
125115

126116
@Nullable
127-
@SuppressWarnings("deprecation")
128-
private Object handleReturnValue(GraphQLContext graphQLContext, @Nullable Object result) {
117+
@SuppressWarnings({"deprecation", "DataFlowIssue"})
118+
private Object handleReturnValue(
119+
GraphQLContext graphQLContext, @Nullable Object result, Method method, Object[] argValues) {
120+
129121
if (this.hasCallableReturnValue && result != null) {
130-
return CompletableFuture.supplyAsync(
131-
() -> {
132-
try {
133-
return ContextSnapshot.captureFrom(graphQLContext).wrap((Callable<?>) result).call();
134-
}
135-
catch (Exception ex) {
136-
throw new IllegalStateException(
137-
"Failure in Callable returned from " + getBridgedMethod().toGenericString(), ex);
138-
}
139-
},
140-
this.executor);
122+
CompletableFuture<Object> future = new CompletableFuture<>();
123+
this.executor.execute(() -> {
124+
try {
125+
ContextSnapshot snapshot = ContextSnapshot.captureFrom(graphQLContext);
126+
Object value = snapshot.wrap((Callable<?>) result).call();
127+
future.complete(value);
128+
}
129+
catch (IllegalArgumentException ex) {
130+
future.completeExceptionally(processIllegalArgumentException(argValues, ex, method));
131+
}
132+
catch (InvocationTargetException ex) {
133+
future.completeExceptionally(processInvocationTargetException(argValues, ex));
134+
}
135+
catch (Exception ex) {
136+
future.completeExceptionally(ex);
137+
}
138+
});
139+
return future;
141140
}
142141
return result;
143142
}
144143

144+
private IllegalStateException processIllegalArgumentException(
145+
Object[] argValues, IllegalArgumentException ex, Method method) {
146+
147+
assertTargetBean(method, getBean(), argValues);
148+
String text = (ex.getMessage() != null) ? ex.getMessage() : "Illegal argument";
149+
return new IllegalStateException(formatInvokeError(text, argValues), ex);
150+
}
151+
152+
private Throwable processInvocationTargetException(Object[] argValues, InvocationTargetException ex) {
153+
// Unwrap for DataFetcherExceptionResolvers ...
154+
Throwable targetException = ex.getTargetException();
155+
if (targetException instanceof Error || targetException instanceof Exception) {
156+
return targetException;
157+
}
158+
String message = formatInvokeError("Invocation failure", argValues);
159+
return new IllegalStateException(message, targetException);
160+
}
161+
145162
/**
146163
* Use this method to resolve the arguments asynchronously. This is only
147164
* useful when at least one of the values is a {@link Mono}

spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/DataFetcherHandlerMethodTests.java

+35-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,20 +19,20 @@
1919

2020
import java.lang.reflect.Method;
2121
import java.util.Collections;
22+
import java.util.Map;
2223
import java.util.concurrent.Callable;
2324
import java.util.concurrent.CompletableFuture;
2425

2526
import graphql.GraphQLContext;
2627
import graphql.schema.DataFetchingEnvironment;
2728
import graphql.schema.DataFetchingEnvironmentImpl;
2829
import org.junit.jupiter.api.Test;
29-
import org.mockito.Mockito;
3030
import reactor.core.publisher.Mono;
31+
import reactor.test.StepVerifier;
3132

3233
import org.springframework.core.task.SimpleAsyncTaskExecutor;
3334
import org.springframework.graphql.data.GraphQlArgumentBinder;
3435
import org.springframework.graphql.data.method.HandlerMethod;
35-
import org.springframework.graphql.data.method.HandlerMethodArgumentResolver;
3636
import org.springframework.graphql.data.method.HandlerMethodArgumentResolverComposite;
3737
import org.springframework.graphql.data.method.annotation.Argument;
3838
import org.springframework.graphql.data.method.annotation.QueryMapping;
@@ -74,14 +74,15 @@ void annotatedMethodsOnInterface() {
7474
void callableReturnValue() throws Exception {
7575

7676
HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite();
77-
resolvers.addResolver(Mockito.mock(HandlerMethodArgumentResolver.class));
77+
resolvers.addResolver(new ArgumentMethodArgumentResolver(new GraphQlArgumentBinder()));
7878

7979
DataFetcherHandlerMethod handlerMethod = new DataFetcherHandlerMethod(
8080
handlerMethodFor(new TestController(), "handleAndReturnCallable"), resolvers, null,
8181
new SimpleAsyncTaskExecutor(), false);
8282

8383
DataFetchingEnvironment environment = DataFetchingEnvironmentImpl
8484
.newDataFetchingEnvironment()
85+
.arguments(Map.of("raiseError", false))
8586
.graphQLContext(GraphQLContext.newContext().build())
8687
.build();
8788

@@ -92,6 +93,29 @@ void callableReturnValue() throws Exception {
9293
assertThat(future.get()).isEqualTo("A");
9394
}
9495

96+
@Test // gh-973
97+
void callableReturnValueWithError() {
98+
99+
HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite();
100+
resolvers.addResolver(new ArgumentMethodArgumentResolver(new GraphQlArgumentBinder()));
101+
102+
DataFetcherHandlerMethod handlerMethod = new DataFetcherHandlerMethod(
103+
handlerMethodFor(new TestController(), "handleAndReturnCallable"), resolvers, null,
104+
new SimpleAsyncTaskExecutor(), false);
105+
106+
DataFetchingEnvironment environment = DataFetchingEnvironmentImpl
107+
.newDataFetchingEnvironment()
108+
.arguments(Map.of("raiseError", true))
109+
.graphQLContext(GraphQLContext.newContext().build())
110+
.build();
111+
112+
Object result = handlerMethod.invoke(environment);
113+
114+
assertThat(result).isInstanceOf(CompletableFuture.class);
115+
CompletableFuture<String> future = (CompletableFuture<String>) result;
116+
StepVerifier.create(Mono.fromFuture(future)).expectErrorMessage("simulated exception").verify();
117+
}
118+
95119
@Test
96120
void completableFutureReturnValue() {
97121

@@ -137,8 +161,13 @@ public String hello(String name) {
137161
}
138162

139163
@Nullable
140-
public Callable<String> handleAndReturnCallable() {
141-
return () -> "A";
164+
public Callable<String> handleAndReturnCallable(@Argument boolean raiseError) {
165+
return () -> {
166+
if (raiseError) {
167+
throw new IllegalStateException("simulated exception");
168+
}
169+
return "A";
170+
};
142171
}
143172

144173
public CompletableFuture<String> handleAndReturnFuture(@AuthenticationPrincipal User user) {

0 commit comments

Comments
 (0)