Skip to content

Commit e179030

Browse files
committed
Inject prepared GraphqlErrorBuilder in error handling methods
Prior to this commit, error handling methods would support various arguments, including the exception being handled. Our reference documentation would advise to create a new instance of a `GraphQLError` using `GraphQLError.newError()`. This does not initialize the location and path information of the current error. This commit allows error handling methods to get injected with a `GraphQlErrorBuilder<?>` argument that is initialized with the current `DataFetchingEnvironment` (thus filling the location and path parts). Fixes gh-1200
1 parent 518617d commit e179030

File tree

7 files changed

+153
-33
lines changed

7 files changed

+153
-33
lines changed

spring-graphql-docs/modules/ROOT/pages/controllers.adoc

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -886,37 +886,14 @@ Use `@GraphQlExceptionHandler` methods to handle exceptions from data fetching w
886886
flexible xref:controllers.adoc#controllers.exception-handler.signature[method signature]. When declared in a
887887
controller, exception handler methods apply to exceptions from the same controller:
888888

889-
[source,java,indent=0,subs="verbatim,quotes"]
890-
----
891-
@Controller
892-
public class BookController {
893-
894-
@QueryMapping
895-
public Book bookById(@Argument Long id) {
896-
// ...
897-
}
898-
899-
@GraphQlExceptionHandler
900-
public GraphQLError handle(BindException ex) {
901-
return GraphQLError.newError().errorType(ErrorType.BAD_REQUEST).message("...").build();
902-
}
903-
}
904-
----
889+
include-code::BookController[]
905890

906891
When declared in an `@ControllerAdvice`, exception handler methods apply across controllers:
907892

908-
[source,java,indent=0,subs="verbatim,quotes"]
909-
----
910-
@ControllerAdvice
911-
public class GlobalExceptionHandler {
893+
include-code::GlobalExceptionHandler[]
912894

913-
@GraphQlExceptionHandler
914-
public GraphQLError handle(BindException ex) {
915-
return GraphQLError.newError().errorType(ErrorType.BAD_REQUEST).message("...").build();
916-
}
917-
918-
}
919-
----
895+
As shown in the examples above, you should build errors by injecting `GraphQlErrorBuilder` in the method signature
896+
because it's been prepared with the current `DataFetchingEnvironment`.
920897

921898
Exception handling via `@GraphQlExceptionHandler` methods is applied automatically to
922899
controller invocations. To handle exceptions from other `graphql.schema.DataFetcher`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2020-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.docs.controllers.exceptionhandler;
18+
19+
public record Book() {
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2020-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.docs.controllers.exceptionhandler;
18+
19+
20+
import graphql.GraphQLError;
21+
import graphql.GraphqlErrorBuilder;
22+
23+
import org.springframework.graphql.data.method.annotation.Argument;
24+
import org.springframework.graphql.data.method.annotation.GraphQlExceptionHandler;
25+
import org.springframework.graphql.data.method.annotation.QueryMapping;
26+
import org.springframework.graphql.execution.ErrorType;
27+
import org.springframework.stereotype.Controller;
28+
import org.springframework.validation.BindException;
29+
30+
@Controller
31+
public class BookController {
32+
33+
@QueryMapping
34+
public Book bookById(@Argument Long id) {
35+
return /**/ new Book();
36+
}
37+
38+
@GraphQlExceptionHandler
39+
public GraphQLError handle(GraphqlErrorBuilder<?> errorBuilder, BindException ex) {
40+
return errorBuilder
41+
.errorType(ErrorType.BAD_REQUEST)
42+
.message(ex.getMessage())
43+
.build();
44+
}
45+
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2020-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.docs.controllers.exceptionhandler;
18+
19+
import graphql.GraphQLError;
20+
import graphql.GraphqlErrorBuilder;
21+
22+
import org.springframework.graphql.data.method.annotation.GraphQlExceptionHandler;
23+
import org.springframework.graphql.execution.ErrorType;
24+
import org.springframework.validation.BindException;
25+
import org.springframework.web.bind.annotation.ControllerAdvice;
26+
27+
@ControllerAdvice
28+
public class GlobalExceptionHandler {
29+
30+
@GraphQlExceptionHandler
31+
public GraphQLError handle(GraphqlErrorBuilder<?> errorBuilder, BindException ex) {
32+
return errorBuilder
33+
.errorType(ErrorType.BAD_REQUEST)
34+
.message(ex.getMessage())
35+
.build();
36+
}
37+
38+
}

spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/DataFetchingEnvironmentMethodArgumentResolver.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2025 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.
@@ -20,6 +20,7 @@
2020
import java.util.Optional;
2121

2222
import graphql.GraphQLContext;
23+
import graphql.GraphqlErrorBuilder;
2324
import graphql.schema.DataFetchingEnvironment;
2425
import graphql.schema.DataFetchingFieldSelectionSet;
2526

@@ -45,6 +46,7 @@ public boolean supportsParameter(MethodParameter parameter) {
4546
Class<?> type = parameter.getParameterType();
4647
return (type.equals(DataFetchingEnvironment.class) || type.equals(GraphQLContext.class) ||
4748
type.equals(DataFetchingFieldSelectionSet.class) ||
49+
type.equals(GraphqlErrorBuilder.class) ||
4850
type.equals(Locale.class) || isOptionalLocale(parameter));
4951
}
5052

@@ -70,6 +72,9 @@ else if (isOptionalLocale(parameter)) {
7072
else if (type.equals(DataFetchingEnvironment.class)) {
7173
return environment;
7274
}
75+
else if (type.equals(GraphqlErrorBuilder.class)) {
76+
return GraphqlErrorBuilder.newError(environment);
77+
}
7378
else {
7479
throw new IllegalStateException("Unexpected method parameter type: " + parameter);
7580
}
Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2025 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.
@@ -21,9 +21,17 @@
2121
import java.util.Optional;
2222

2323
import graphql.GraphQLContext;
24+
import graphql.GraphQLError;
25+
import graphql.GraphqlErrorBuilder;
26+
import graphql.execution.ExecutionStepInfo;
27+
import graphql.execution.MergedField;
28+
import graphql.execution.ResultPath;
29+
import graphql.language.Field;
30+
import graphql.language.SourceLocation;
2431
import graphql.schema.DataFetchingEnvironment;
2532
import graphql.schema.DataFetchingEnvironmentImpl;
2633
import graphql.schema.DataFetchingFieldSelectionSet;
34+
import graphql.schema.GraphQLObjectType;
2735
import org.junit.jupiter.api.Test;
2836

2937
import org.springframework.core.MethodParameter;
@@ -35,11 +43,12 @@
3543
/**
3644
* Unit tests for {@link DataFetchingEnvironmentMethodArgumentResolver}.
3745
* @author Rossen Stoyanchev
46+
* @author Brian Clozel
3847
*/
39-
public class DataFetchingEnvironmentArgumentResolverTests {
48+
public class DataFetchingEnvironmentMethodArgumentResolverTests {
4049

4150
private static final Method handleMethod = ClassUtils.getMethod(
42-
DataFetchingEnvironmentArgumentResolverTests.class, "handle", (Class<?>[]) null);
51+
DataFetchingEnvironmentMethodArgumentResolverTests.class, "handle", (Class<?>[]) null);
4352

4453

4554
private final DataFetchingEnvironmentMethodArgumentResolver resolver =
@@ -51,8 +60,9 @@ void supportsParameter() {
5160
assertThat(this.resolver.supportsParameter(parameter(1))).isTrue();
5261
assertThat(this.resolver.supportsParameter(parameter(2))).isTrue();
5362
assertThat(this.resolver.supportsParameter(parameter(3))).isTrue();
63+
assertThat(this.resolver.supportsParameter(parameter(4))).isTrue();
5464

55-
assertThat(this.resolver.supportsParameter(parameter(4))).isFalse();
65+
assertThat(this.resolver.supportsParameter(parameter(5))).isFalse();
5666
}
5767

5868
@Test
@@ -94,6 +104,25 @@ void resolveOptionalLocale() {
94104
assertThat(actual.get()).isSameAs(locale);
95105
}
96106

107+
@Test
108+
void resolveErrorBuilder() {
109+
MergedField field = MergedField.newMergedField(Field.newField("greeting")
110+
.sourceLocation(SourceLocation.EMPTY).build()).build();
111+
ExecutionStepInfo executionStepInfo = ExecutionStepInfo.newExecutionStepInfo()
112+
.path(ResultPath.parse("/greeting"))
113+
.type(new GraphQLObjectType.Builder().name("project").build()).build();
114+
DataFetchingEnvironment environment = environment()
115+
.mergedField(field)
116+
.executionStepInfo(executionStepInfo)
117+
.build();
118+
119+
GraphqlErrorBuilder<?> errorBuilder = (GraphqlErrorBuilder<?>) this.resolver.resolveArgument(parameter(4), environment);
120+
GraphQLError error = errorBuilder.message("custom error message").build();
121+
122+
assertThat(ResultPath.fromList(error.getPath()).toString()).isEqualTo("/greeting");
123+
assertThat(error.getLocations()).isNotEmpty();
124+
}
125+
97126
private static DataFetchingEnvironmentImpl.Builder environment() {
98127
return DataFetchingEnvironmentImpl.newDataFetchingEnvironment();
99128
}
@@ -109,6 +138,7 @@ public void handle(
109138
DataFetchingFieldSelectionSet selectionSet,
110139
Locale locale,
111140
Optional<Locale> optionalLocale,
141+
GraphqlErrorBuilder<?> errorBuilder,
112142
String s) {
113143
}
114144

spring-graphql/src/test/java/org/springframework/graphql/execution/ExceptionResolversExceptionHandlerTests.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 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.
@@ -64,6 +64,7 @@ void resolveException() throws Exception {
6464
assertThat(response.errorCount()).isEqualTo(1);
6565
assertThat(response.error(0).message()).isEqualTo("Resolved error: Invalid greeting");
6666
assertThat(response.error(0).errorType()).isEqualTo("BAD_REQUEST");
67+
assertThat(response.error(0).path()).isEqualTo("/greeting");
6768

6869
String greeting = response.rawValue("greeting");
6970
assertThat(greeting).isNull();
@@ -85,6 +86,7 @@ void resolveExceptionWithReactorContext() throws Exception {
8586
ResponseHelper response = ResponseHelper.forResult(result);
8687
assertThat(response.errorCount()).isEqualTo(1);
8788
assertThat(response.error(0).message()).isEqualTo("Resolved error: Invalid greeting, name=007");
89+
assertThat(response.error(0).path()).isEqualTo("/greeting");
8890
}
8991

9092
@Test
@@ -110,6 +112,7 @@ void resolveExceptionWithThreadLocal() {
110112
ResponseHelper response = ResponseHelper.forResult(result);
111113
assertThat(response.errorCount()).isEqualTo(1);
112114
assertThat(response.error(0).message()).isEqualTo("Resolved error: Invalid greeting, name=007");
115+
assertThat(response.error(0).path()).isEqualTo("/greeting");
113116
}
114117
finally {
115118
threadLocal.remove();
@@ -127,6 +130,7 @@ void unresolvedException() throws Exception {
127130
ResponseHelper response = ResponseHelper.forResult(result);
128131
assertThat(response.errorCount()).isEqualTo(1);
129132
assertThat(response.error(0).message()).startsWith("INTERNAL_ERROR for ");
133+
assertThat(response.error(0).path()).isEqualTo("/greeting");
130134
assertThat(response.error(0).errorType()).isEqualTo("INTERNAL_ERROR");
131135

132136
String greeting = response.rawValue("greeting");

0 commit comments

Comments
 (0)