diff --git a/spring-graphql-docs/modules/ROOT/pages/controllers.adoc b/spring-graphql-docs/modules/ROOT/pages/controllers.adoc index 2e0b384c..b397fc9b 100644 --- a/spring-graphql-docs/modules/ROOT/pages/controllers.adoc +++ b/spring-graphql-docs/modules/ROOT/pages/controllers.adoc @@ -886,37 +886,14 @@ Use `@GraphQlExceptionHandler` methods to handle exceptions from data fetching w flexible xref:controllers.adoc#controllers.exception-handler.signature[method signature]. When declared in a controller, exception handler methods apply to exceptions from the same controller: -[source,java,indent=0,subs="verbatim,quotes"] ----- - @Controller - public class BookController { - - @QueryMapping - public Book bookById(@Argument Long id) { - // ... - } - - @GraphQlExceptionHandler - public GraphQLError handle(BindException ex) { - return GraphQLError.newError().errorType(ErrorType.BAD_REQUEST).message("...").build(); - } - } ----- +include-code::BookController[] When declared in an `@ControllerAdvice`, exception handler methods apply across controllers: -[source,java,indent=0,subs="verbatim,quotes"] ----- - @ControllerAdvice - public class GlobalExceptionHandler { +include-code::GlobalExceptionHandler[] - @GraphQlExceptionHandler - public GraphQLError handle(BindException ex) { - return GraphQLError.newError().errorType(ErrorType.BAD_REQUEST).message("...").build(); - } - - } ----- +As shown in the examples above, you should build errors by injecting `GraphQlErrorBuilder` in the method signature +because it's been prepared with the current `DataFetchingEnvironment`. Exception handling via `@GraphQlExceptionHandler` methods is applied automatically to controller invocations. To handle exceptions from other `graphql.schema.DataFetcher` diff --git a/spring-graphql-docs/src/main/java/org/springframework/graphql/docs/controllers/exceptionhandler/Book.java b/spring-graphql-docs/src/main/java/org/springframework/graphql/docs/controllers/exceptionhandler/Book.java new file mode 100644 index 00000000..781b1b0b --- /dev/null +++ b/spring-graphql-docs/src/main/java/org/springframework/graphql/docs/controllers/exceptionhandler/Book.java @@ -0,0 +1,20 @@ +/* + * Copyright 2020-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.graphql.docs.controllers.exceptionhandler; + +public record Book() { +} diff --git a/spring-graphql-docs/src/main/java/org/springframework/graphql/docs/controllers/exceptionhandler/BookController.java b/spring-graphql-docs/src/main/java/org/springframework/graphql/docs/controllers/exceptionhandler/BookController.java new file mode 100644 index 00000000..f424e86f --- /dev/null +++ b/spring-graphql-docs/src/main/java/org/springframework/graphql/docs/controllers/exceptionhandler/BookController.java @@ -0,0 +1,46 @@ +/* + * Copyright 2020-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.graphql.docs.controllers.exceptionhandler; + + +import graphql.GraphQLError; +import graphql.GraphqlErrorBuilder; + +import org.springframework.graphql.data.method.annotation.Argument; +import org.springframework.graphql.data.method.annotation.GraphQlExceptionHandler; +import org.springframework.graphql.data.method.annotation.QueryMapping; +import org.springframework.graphql.execution.ErrorType; +import org.springframework.stereotype.Controller; +import org.springframework.validation.BindException; + +@Controller +public class BookController { + + @QueryMapping + public Book bookById(@Argument Long id) { + return /**/ new Book(); + } + + @GraphQlExceptionHandler + public GraphQLError handle(GraphqlErrorBuilder errorBuilder, BindException ex) { + return errorBuilder + .errorType(ErrorType.BAD_REQUEST) + .message(ex.getMessage()) + .build(); + } + +} diff --git a/spring-graphql-docs/src/main/java/org/springframework/graphql/docs/controllers/exceptionhandler/GlobalExceptionHandler.java b/spring-graphql-docs/src/main/java/org/springframework/graphql/docs/controllers/exceptionhandler/GlobalExceptionHandler.java new file mode 100644 index 00000000..4b32a129 --- /dev/null +++ b/spring-graphql-docs/src/main/java/org/springframework/graphql/docs/controllers/exceptionhandler/GlobalExceptionHandler.java @@ -0,0 +1,38 @@ +/* + * Copyright 2020-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.graphql.docs.controllers.exceptionhandler; + +import graphql.GraphQLError; +import graphql.GraphqlErrorBuilder; + +import org.springframework.graphql.data.method.annotation.GraphQlExceptionHandler; +import org.springframework.graphql.execution.ErrorType; +import org.springframework.validation.BindException; +import org.springframework.web.bind.annotation.ControllerAdvice; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @GraphQlExceptionHandler + public GraphQLError handle(GraphqlErrorBuilder errorBuilder, BindException ex) { + return errorBuilder + .errorType(ErrorType.BAD_REQUEST) + .message(ex.getMessage()) + .build(); + } + +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/DataFetchingEnvironmentMethodArgumentResolver.java b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/DataFetchingEnvironmentMethodArgumentResolver.java index 577a9e4c..1bef3e3a 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/DataFetchingEnvironmentMethodArgumentResolver.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/method/annotation/support/DataFetchingEnvironmentMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import java.util.Locale; import java.util.Optional; import graphql.GraphQLContext; +import graphql.GraphqlErrorBuilder; import graphql.schema.DataFetchingEnvironment; import graphql.schema.DataFetchingFieldSelectionSet; @@ -45,6 +46,7 @@ public class DataFetchingEnvironmentMethodArgumentResolver implements HandlerMet Class type = parameter.getParameterType(); return (type.equals(DataFetchingEnvironment.class) || type.equals(GraphQLContext.class) || type.equals(DataFetchingFieldSelectionSet.class) || + type.equals(GraphqlErrorBuilder.class) || type.equals(Locale.class) || isOptionalLocale(parameter)); } @@ -70,6 +72,9 @@ public class DataFetchingEnvironmentMethodArgumentResolver implements HandlerMet else if (type.equals(DataFetchingEnvironment.class)) { return environment; } + else if (type.equals(GraphqlErrorBuilder.class)) { + return GraphqlErrorBuilder.newError(environment); + } else { throw new IllegalStateException("Unexpected method parameter type: " + parameter); } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/DataFetchingEnvironmentArgumentResolverTests.java b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/DataFetchingEnvironmentMethodArgumentResolverTests.java similarity index 69% rename from spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/DataFetchingEnvironmentArgumentResolverTests.java rename to spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/DataFetchingEnvironmentMethodArgumentResolverTests.java index c4dc8d6b..1fb3c70b 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/DataFetchingEnvironmentArgumentResolverTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/data/method/annotation/support/DataFetchingEnvironmentMethodArgumentResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,9 +21,17 @@ import java.util.Locale; import java.util.Optional; import graphql.GraphQLContext; +import graphql.GraphQLError; +import graphql.GraphqlErrorBuilder; +import graphql.execution.ExecutionStepInfo; +import graphql.execution.MergedField; +import graphql.execution.ResultPath; +import graphql.language.Field; +import graphql.language.SourceLocation; import graphql.schema.DataFetchingEnvironment; import graphql.schema.DataFetchingEnvironmentImpl; import graphql.schema.DataFetchingFieldSelectionSet; +import graphql.schema.GraphQLObjectType; import org.junit.jupiter.api.Test; import org.springframework.core.MethodParameter; @@ -35,11 +43,12 @@ import static org.mockito.Mockito.mock; /** * Unit tests for {@link DataFetchingEnvironmentMethodArgumentResolver}. * @author Rossen Stoyanchev + * @author Brian Clozel */ -public class DataFetchingEnvironmentArgumentResolverTests { +public class DataFetchingEnvironmentMethodArgumentResolverTests { private static final Method handleMethod = ClassUtils.getMethod( - DataFetchingEnvironmentArgumentResolverTests.class, "handle", (Class[]) null); + DataFetchingEnvironmentMethodArgumentResolverTests.class, "handle", (Class[]) null); private final DataFetchingEnvironmentMethodArgumentResolver resolver = @@ -51,8 +60,9 @@ public class DataFetchingEnvironmentArgumentResolverTests { assertThat(this.resolver.supportsParameter(parameter(1))).isTrue(); assertThat(this.resolver.supportsParameter(parameter(2))).isTrue(); assertThat(this.resolver.supportsParameter(parameter(3))).isTrue(); + assertThat(this.resolver.supportsParameter(parameter(4))).isTrue(); - assertThat(this.resolver.supportsParameter(parameter(4))).isFalse(); + assertThat(this.resolver.supportsParameter(parameter(5))).isFalse(); } @Test @@ -94,6 +104,25 @@ public class DataFetchingEnvironmentArgumentResolverTests { assertThat(actual.get()).isSameAs(locale); } + @Test + void resolveErrorBuilder() { + MergedField field = MergedField.newMergedField(Field.newField("greeting") + .sourceLocation(SourceLocation.EMPTY).build()).build(); + ExecutionStepInfo executionStepInfo = ExecutionStepInfo.newExecutionStepInfo() + .path(ResultPath.parse("/greeting")) + .type(new GraphQLObjectType.Builder().name("project").build()).build(); + DataFetchingEnvironment environment = environment() + .mergedField(field) + .executionStepInfo(executionStepInfo) + .build(); + + GraphqlErrorBuilder errorBuilder = (GraphqlErrorBuilder) this.resolver.resolveArgument(parameter(4), environment); + GraphQLError error = errorBuilder.message("custom error message").build(); + + assertThat(ResultPath.fromList(error.getPath()).toString()).isEqualTo("/greeting"); + assertThat(error.getLocations()).isNotEmpty(); + } + private static DataFetchingEnvironmentImpl.Builder environment() { return DataFetchingEnvironmentImpl.newDataFetchingEnvironment(); } @@ -109,6 +138,7 @@ public class DataFetchingEnvironmentArgumentResolverTests { DataFetchingFieldSelectionSet selectionSet, Locale locale, Optional optionalLocale, + GraphqlErrorBuilder errorBuilder, String s) { } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/execution/ExceptionResolversExceptionHandlerTests.java b/spring-graphql/src/test/java/org/springframework/graphql/execution/ExceptionResolversExceptionHandlerTests.java index 35140500..0ce20bf5 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/execution/ExceptionResolversExceptionHandlerTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/execution/ExceptionResolversExceptionHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -64,6 +64,7 @@ public class ExceptionResolversExceptionHandlerTests { assertThat(response.errorCount()).isEqualTo(1); assertThat(response.error(0).message()).isEqualTo("Resolved error: Invalid greeting"); assertThat(response.error(0).errorType()).isEqualTo("BAD_REQUEST"); + assertThat(response.error(0).path()).isEqualTo("/greeting"); String greeting = response.rawValue("greeting"); assertThat(greeting).isNull(); @@ -85,6 +86,7 @@ public class ExceptionResolversExceptionHandlerTests { ResponseHelper response = ResponseHelper.forResult(result); assertThat(response.errorCount()).isEqualTo(1); assertThat(response.error(0).message()).isEqualTo("Resolved error: Invalid greeting, name=007"); + assertThat(response.error(0).path()).isEqualTo("/greeting"); } @Test @@ -110,6 +112,7 @@ public class ExceptionResolversExceptionHandlerTests { ResponseHelper response = ResponseHelper.forResult(result); assertThat(response.errorCount()).isEqualTo(1); assertThat(response.error(0).message()).isEqualTo("Resolved error: Invalid greeting, name=007"); + assertThat(response.error(0).path()).isEqualTo("/greeting"); } finally { threadLocal.remove(); @@ -127,6 +130,7 @@ public class ExceptionResolversExceptionHandlerTests { ResponseHelper response = ResponseHelper.forResult(result); assertThat(response.errorCount()).isEqualTo(1); assertThat(response.error(0).message()).startsWith("INTERNAL_ERROR for "); + assertThat(response.error(0).path()).isEqualTo("/greeting"); assertThat(response.error(0).errorType()).isEqualTo("INTERNAL_ERROR"); String greeting = response.rawValue("greeting");