diff --git a/client/generator/pom.xml b/client/generator/pom.xml
index 5f06a3770..67f90b1fd 100644
--- a/client/generator/pom.xml
+++ b/client/generator/pom.xml
@@ -67,12 +67,6 @@
${project.version}
provided
-
- io.smallrye
- smallrye-graphql-api
- ${project.version}
- provided
-
com.graphql-java
graphql-java
diff --git a/server/api/src/main/java/io/smallrye/graphql/api/ErrorCode.java b/server/api/src/main/java/io/smallrye/graphql/api/ErrorCode.java
new file mode 100644
index 000000000..faa28543d
--- /dev/null
+++ b/server/api/src/main/java/io/smallrye/graphql/api/ErrorCode.java
@@ -0,0 +1,14 @@
+package io.smallrye.graphql.api;
+
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Retention(RUNTIME)
+@Target({ TYPE, ANNOTATION_TYPE })
+public @interface ErrorCode {
+ String value();
+}
diff --git a/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ExecutionErrorsService.java b/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ExecutionErrorsService.java
index 175c62c23..5193a3b9b 100644
--- a/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ExecutionErrorsService.java
+++ b/server/implementation/src/main/java/io/smallrye/graphql/execution/error/ExecutionErrorsService.java
@@ -1,5 +1,7 @@
package io.smallrye.graphql.execution.error;
+import static java.util.Locale.UK;
+
import java.io.StringReader;
import java.util.List;
import java.util.Map;
@@ -20,6 +22,7 @@
import graphql.ExceptionWhileDataFetching;
import graphql.GraphQLError;
import graphql.validation.ValidationError;
+import io.smallrye.graphql.api.ErrorCode;
/**
* Help to create the exceptions
@@ -50,10 +53,7 @@ private JsonObject toJsonError(GraphQLError error) {
JsonObjectBuilder resultBuilder = jsonBuilderFactory.createObjectBuilder(jsonErrors);
- Optional optionalExtensions = getOptionalExtensions(error);
- if (optionalExtensions.isPresent()) {
- resultBuilder.add(EXTENSIONS, optionalExtensions.get());
- }
+ getOptionalExtensions(error).ifPresent(jsonObject -> resultBuilder.add(EXTENSIONS, jsonObject));
return resultBuilder.build();
}
}
@@ -86,12 +86,26 @@ private Optional getDataFetchingExtensions(ExceptionWhileDataFetchin
addKeyValue(objectBuilder, EXCEPTION, exception.getClass().getName());
addKeyValue(objectBuilder, CLASSIFICATION, error.getErrorType().toString());
+ addKeyValue(objectBuilder, CODE, toErrorCode(exception));
Map extensions = error.getExtensions();
populateCustomExtensions(objectBuilder, extensions);
return Optional.of(objectBuilder.build());
}
+ private String toErrorCode(Throwable exception) {
+ // TODO change to use the Model and the model needs to get this using Jandex
+ ErrorCode annotation = exception.getClass().getAnnotation(ErrorCode.class);
+ return (annotation == null)
+ ? camelToKebab(exception.getClass().getSimpleName().replaceAll("Exception$", ""))
+ : annotation.value();
+ }
+
+ private static String camelToKebab(String input) {
+ return String.join("-", input.split("(?=\\p{javaUpperCase})"))
+ .toLowerCase(UK);
+ }
+
private void populateCustomExtensions(JsonObjectBuilder objectBuilder, Map extensions) {
if (extensions != null) {
for (Map.Entry entry : extensions.entrySet()) {
@@ -121,5 +135,6 @@ private void addKeyValue(JsonObjectBuilder objectBuilder, String key, String val
private static final String QUERYPATH = "queryPath";
private static final String CLASSIFICATION = "classification";
private static final String EXTENSIONS = "extensions";
+ private static final String CODE = "code";
}
diff --git a/server/implementation/src/test/java/io/smallrye/graphql/execution/error/ExecutionErrorsServiceTest.java b/server/implementation/src/test/java/io/smallrye/graphql/execution/error/ExecutionErrorsServiceTest.java
index 378da0146..83f3417e5 100644
--- a/server/implementation/src/test/java/io/smallrye/graphql/execution/error/ExecutionErrorsServiceTest.java
+++ b/server/implementation/src/test/java/io/smallrye/graphql/execution/error/ExecutionErrorsServiceTest.java
@@ -1,23 +1,24 @@
package io.smallrye.graphql.execution.error;
+import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat;
-import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.json.JsonArray;
import javax.json.JsonObject;
-import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import graphql.ExceptionWhileDataFetching;
+import graphql.GraphQLError;
import graphql.GraphqlErrorException;
import graphql.execution.ExecutionPath;
import graphql.language.SourceLocation;
import graphql.validation.ValidationError;
import graphql.validation.ValidationErrorType;
+import io.smallrye.graphql.api.ErrorCode;
/**
* Test for {@link ExecutionErrorsService}
@@ -26,18 +27,13 @@
*/
class ExecutionErrorsServiceTest {
- private ExecutionErrorsService executionErrorsService;
-
- @BeforeEach
- void init() {
- executionErrorsService = new ExecutionErrorsService();
- }
+ private final ExecutionErrorsService executionErrorsService = new ExecutionErrorsService();
@Test
void testToJsonErrors_WhenExceptionWhileDataFetchingErrorCaught_ShouldReturnJsonBodyWithCustomExtensions() {
// Given
Map extensions = new HashMap<>();
- extensions.put("errorCode", "OPERATION_FAILED");
+ extensions.put("code", "OPERATION_FAILED");
GraphqlErrorException graphqlErrorException = GraphqlErrorException.newErrorException()
.extensions(extensions)
.build();
@@ -45,29 +41,29 @@ void testToJsonErrors_WhenExceptionWhileDataFetchingErrorCaught_ShouldReturnJson
graphqlErrorException, new SourceLocation(1, 1));
// When
- JsonArray jsonArray = executionErrorsService.toJsonErrors(Collections.singletonList(exceptionWhileDataFetching));
+ JsonArray jsonArray = executionErrorsService.toJsonErrors(singletonList(exceptionWhileDataFetching));
// Then
JsonObject extensionJsonObject = jsonArray.getJsonObject(0).getJsonObject("extensions");
assertThat(extensionJsonObject.getString("exception")).isEqualTo("graphql.GraphqlErrorException");
assertThat(extensionJsonObject.getString("classification")).isEqualTo("DataFetchingException");
- assertThat(extensionJsonObject.getString("errorCode")).isEqualTo("OPERATION_FAILED");
+ assertThat(extensionJsonObject.getString("code")).isEqualTo("OPERATION_FAILED");
}
@Test
void testToJsonErrors_WhenExceptionWhileValidationErrorCaught_ShouldReturnJsonBodyWithCustomExtensions() {
// Given
Map extensions = new HashMap<>();
- extensions.put("errorCode", "OPERATION_FAILED");
+ extensions.put("code", "OPERATION_FAILED");
ValidationError validationError = ValidationError.newValidationError()
.validationErrorType(ValidationErrorType.UnknownDirective)
.description("TestDescription")
- .queryPath(Collections.singletonList("Test-Path"))
+ .queryPath(singletonList("Test-Path"))
.extensions(extensions)
.build();
// When
- JsonArray jsonArray = executionErrorsService.toJsonErrors(Collections.singletonList(validationError));
+ JsonArray jsonArray = executionErrorsService.toJsonErrors(singletonList(validationError));
// Then
JsonObject extensionJsonObject = jsonArray.getJsonObject(0).getJsonObject("extensions");
@@ -75,6 +71,44 @@ void testToJsonErrors_WhenExceptionWhileValidationErrorCaught_ShouldReturnJsonBo
assertThat(extensionJsonObject.getString("validationErrorType")).isEqualTo("UnknownDirective");
assertThat(extensionJsonObject.getJsonArray("queryPath").getString(0)).isEqualTo("Test-Path");
assertThat(extensionJsonObject.getString("classification")).isEqualTo("ValidationError");
- assertThat(extensionJsonObject.getString("errorCode")).isEqualTo("OPERATION_FAILED");
+ assertThat(extensionJsonObject.getString("code")).isEqualTo("OPERATION_FAILED");
+ }
+
+ @Test
+ void shouldMapExceptionNameToCode() {
+ class DummyBusinessException extends RuntimeException {
+ public DummyBusinessException(String message) {
+ super(message);
+ }
+ }
+
+ JsonArray jsonArray = whenConverting(new DummyBusinessException("dummy-message"));
+
+ JsonObject extensions = jsonArray.getJsonObject(0).getJsonObject("extensions");
+ assertThat(extensions.getString("exception")).isEqualTo(DummyBusinessException.class.getName());
+ assertThat(extensions.getString("code", null)).isEqualTo("dummy-business");
+ }
+
+ @Test
+ void shouldMapClassAnnotationErrorCode() {
+ @ErrorCode("dummy-code")
+ class DummyBusinessException extends RuntimeException {
+ public DummyBusinessException(String message) {
+ super(message);
+ }
+ }
+
+ JsonArray jsonArray = whenConverting(new DummyBusinessException("dummy-message"));
+
+ JsonObject extensions = jsonArray.getJsonObject(0).getJsonObject("extensions");
+ assertThat(extensions.getString("exception")).isEqualTo(DummyBusinessException.class.getName());
+ assertThat(extensions.getString("code", null)).isEqualTo("dummy-code");
+ }
+
+ private JsonArray whenConverting(RuntimeException exception) {
+ ExecutionPath path = ExecutionPath.parse("/foo/bar");
+ SourceLocation location = new SourceLocation(12, 34);
+ GraphQLError graphQLError = new GraphQLExceptionWhileDataFetching(path, exception, location);
+ return executionErrorsService.toJsonErrors(singletonList(graphQLError));
}
}
diff --git a/server/tck/src/test/java/io/smallrye/graphql/SmallRyeGraphQLArchiveProcessor.java b/server/tck/src/test/java/io/smallrye/graphql/SmallRyeGraphQLArchiveProcessor.java
index b28531d20..447610658 100644
--- a/server/tck/src/test/java/io/smallrye/graphql/SmallRyeGraphQLArchiveProcessor.java
+++ b/server/tck/src/test/java/io/smallrye/graphql/SmallRyeGraphQLArchiveProcessor.java
@@ -28,6 +28,7 @@
import org.jboss.shrinkwrap.resolver.api.maven.Maven;
import io.smallrye.graphql.test.apps.async.api.AsyncApi;
+import io.smallrye.graphql.test.apps.error.api.ErrorApi;
import io.smallrye.graphql.test.apps.profile.api.ProfileGraphQLApi;
import io.smallrye.graphql.test.apps.scalars.api.AdditionalScalarsApi;
@@ -70,6 +71,7 @@ public void process(Archive> applicationArchive, TestClass testClass) {
testDeployment.addPackage(ProfileGraphQLApi.class.getPackage());
testDeployment.addPackage(AdditionalScalarsApi.class.getPackage());
testDeployment.addPackage(AsyncApi.class.getPackage());
+ testDeployment.addPackage(ErrorApi.class.getPackage());
}
}
}
diff --git a/server/tck/src/test/java/io/smallrye/graphql/test/apps/error/api/ErrorApi.java b/server/tck/src/test/java/io/smallrye/graphql/test/apps/error/api/ErrorApi.java
new file mode 100644
index 000000000..c82d4b3c7
--- /dev/null
+++ b/server/tck/src/test/java/io/smallrye/graphql/test/apps/error/api/ErrorApi.java
@@ -0,0 +1,31 @@
+package io.smallrye.graphql.test.apps.error.api;
+
+import org.eclipse.microprofile.graphql.GraphQLApi;
+import org.eclipse.microprofile.graphql.Query;
+
+import io.smallrye.graphql.api.ErrorCode;
+
+@GraphQLApi
+public class ErrorApi {
+ @Query
+ public String unsupportedOperation() {
+ throw new UnsupportedOperationException("dummy-message");
+ }
+
+ public static class CustomBusinessException extends RuntimeException {
+ }
+
+ @Query
+ public String customBusinessException() {
+ throw new CustomBusinessException();
+ }
+
+ @ErrorCode("some-business-error-code")
+ public static class AnnotatedCustomBusinessException extends RuntimeException {
+ }
+
+ @Query
+ public String annotatedCustomBusinessException() {
+ throw new AnnotatedCustomBusinessException();
+ }
+}
diff --git a/server/tck/src/test/resources/tests/errors/annotatedCustomException/input.graphql b/server/tck/src/test/resources/tests/errors/annotatedCustomException/input.graphql
new file mode 100644
index 000000000..214614c53
--- /dev/null
+++ b/server/tck/src/test/resources/tests/errors/annotatedCustomException/input.graphql
@@ -0,0 +1,3 @@
+{
+ annotatedCustomBusinessException
+}
diff --git a/server/tck/src/test/resources/tests/errors/annotatedCustomException/output.json b/server/tck/src/test/resources/tests/errors/annotatedCustomException/output.json
new file mode 100644
index 000000000..10f3f400a
--- /dev/null
+++ b/server/tck/src/test/resources/tests/errors/annotatedCustomException/output.json
@@ -0,0 +1,24 @@
+{
+ "errors": [
+ {
+ "message": "Unexpected failure in the system. Jarvis is working to fix it.",
+ "locations": [
+ {
+ "line": 2,
+ "column": 3
+ }
+ ],
+ "path": [
+ "annotatedCustomBusinessException"
+ ],
+ "extensions": {
+ "exception": "io.smallrye.graphql.test.apps.error.api.ErrorApi$AnnotatedCustomBusinessException",
+ "classification": "DataFetchingException",
+ "code": "some-business-error-code"
+ }
+ }
+ ],
+ "data": {
+ "annotatedCustomBusinessException": null
+ }
+}
diff --git a/server/tck/src/test/resources/tests/errors/annotatedCustomException/test.properties b/server/tck/src/test/resources/tests/errors/annotatedCustomException/test.properties
new file mode 100644
index 000000000..faa5dd321
--- /dev/null
+++ b/server/tck/src/test/resources/tests/errors/annotatedCustomException/test.properties
@@ -0,0 +1,4 @@
+## error extension `code` derived from the `@GraphQlErrorCode` annotation on a custom exception
+
+ignore=false
+priority=100
diff --git a/server/tck/src/test/resources/tests/errors/builtInException/input.graphql b/server/tck/src/test/resources/tests/errors/builtInException/input.graphql
new file mode 100644
index 000000000..c90e34926
--- /dev/null
+++ b/server/tck/src/test/resources/tests/errors/builtInException/input.graphql
@@ -0,0 +1,3 @@
+{
+ unsupportedOperation
+}
diff --git a/server/tck/src/test/resources/tests/errors/builtInException/output.json b/server/tck/src/test/resources/tests/errors/builtInException/output.json
new file mode 100644
index 000000000..b62440f86
--- /dev/null
+++ b/server/tck/src/test/resources/tests/errors/builtInException/output.json
@@ -0,0 +1,24 @@
+{
+ "errors": [
+ {
+ "message": "Unexpected failure in the system. Jarvis is working to fix it.",
+ "locations": [
+ {
+ "line": 2,
+ "column": 3
+ }
+ ],
+ "path": [
+ "unsupportedOperation"
+ ],
+ "extensions": {
+ "exception": "java.lang.UnsupportedOperationException",
+ "classification": "DataFetchingException",
+ "code": "unsupported-operation"
+ }
+ }
+ ],
+ "data": {
+ "unsupportedOperation": null
+ }
+}
diff --git a/server/tck/src/test/resources/tests/errors/builtInException/test.properties b/server/tck/src/test/resources/tests/errors/builtInException/test.properties
new file mode 100644
index 000000000..dd460341c
--- /dev/null
+++ b/server/tck/src/test/resources/tests/errors/builtInException/test.properties
@@ -0,0 +1,4 @@
+## error extension `code` derived from the class name of a jdk exception
+
+ignore=false
+priority=100
diff --git a/server/tck/src/test/resources/tests/errors/customException/input.graphql b/server/tck/src/test/resources/tests/errors/customException/input.graphql
new file mode 100644
index 000000000..46cea77b4
--- /dev/null
+++ b/server/tck/src/test/resources/tests/errors/customException/input.graphql
@@ -0,0 +1,3 @@
+{
+ customBusinessException
+}
diff --git a/server/tck/src/test/resources/tests/errors/customException/output.json b/server/tck/src/test/resources/tests/errors/customException/output.json
new file mode 100644
index 000000000..11cc17c3e
--- /dev/null
+++ b/server/tck/src/test/resources/tests/errors/customException/output.json
@@ -0,0 +1,24 @@
+{
+ "errors": [
+ {
+ "message": "Unexpected failure in the system. Jarvis is working to fix it.",
+ "locations": [
+ {
+ "line": 2,
+ "column": 3
+ }
+ ],
+ "path": [
+ "customBusinessException"
+ ],
+ "extensions": {
+ "exception": "io.smallrye.graphql.test.apps.error.api.ErrorApi$CustomBusinessException",
+ "classification": "DataFetchingException",
+ "code": "custom-business"
+ }
+ }
+ ],
+ "data": {
+ "customBusinessException": null
+ }
+}
diff --git a/server/tck/src/test/resources/tests/errors/customException/test.properties b/server/tck/src/test/resources/tests/errors/customException/test.properties
new file mode 100644
index 000000000..eb78b031e
--- /dev/null
+++ b/server/tck/src/test/resources/tests/errors/customException/test.properties
@@ -0,0 +1,4 @@
+## error extension `code` derived from the class name of a custom exception
+
+ignore=false
+priority=100