Skip to content

Commit bb8c54c

Browse files
authored
A root and branch review of error messages. (#650)
* A root and branch review of error messages. Now with toSpecification and extensions defined on them as well as an optional path * Fixed some tests that I missed * Documentation and made extensions be <String,Object> * PR feedback - moved helper into its own class
1 parent 02f34e6 commit bb8c54c

22 files changed

+620
-122
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ dependencies {
3939
testCompile 'org.objenesis:objenesis:2.1'
4040
testCompile 'com.google.code.gson:gson:2.8.0'
4141
testCompile 'org.eclipse.jetty:jetty-server:9.4.5.v20170502'
42+
testCompile 'com.fasterxml.jackson.core:jackson-databind:2.8.8.1'
4243
testCompile 'org.slf4j:slf4j-simple:' + slf4jVersion
4344
testCompile 'org.awaitility:awaitility-groovy:3.0.0'
4445
testCompile 'com.graphql-java:java-dataloader:1.0.1'

docs/execution.rst

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,36 @@ Here is the code for the standard behaviour.
8989
}
9090
}
9191
92+
If the exception you throw is itself a `GraphqlError` then it will transfer the message and custom extensions attributes from that exception
93+
into the `ExceptionWhileDataFetching` object. This allows you to place your own custom attributes into the graphql error that is sent back
94+
to the caller.
95+
96+
For example imagine your data fetcher threw this exception. The `foo` and `fizz` attributes would be included in the resultant
97+
graphql error.
98+
99+
.. code-block:: java
100+
101+
class CustomRuntimeException extends RuntimeException implements GraphQLError {
102+
@Override
103+
public Map<String, Object> getExtensions() {
104+
Map<String, Object> customAttributes = new LinkedHashMap<>();
105+
customAttributes.put("foo", "bar");
106+
customAttributes.put("fizz", "whizz");
107+
return customAttributes;
108+
}
109+
110+
@Override
111+
public List<SourceLocation> getLocations() {
112+
return null;
113+
}
114+
115+
@Override
116+
public ErrorType getErrorType() {
117+
return ErrorType.DataFetchingException;
118+
}
119+
}
120+
121+
92122
You can change this behaviour by creating your own ``graphql.execution.DataFetcherExceptionHandler`` exception handling code and
93123
giving that to the execution strategy.
94124

@@ -107,6 +137,31 @@ behaviour.
107137
};
108138
ExecutionStrategy executionStrategy = new AsyncExecutionStrategy(handler);
109139
140+
Serializing results to JSON
141+
---------------------------
142+
143+
The most common way to call graphql is over HTTP and to expect a JSON response back. So you need to turn an
144+
`graphql.ExecutionResult` into a JSON payload.
145+
146+
A common way to do that is use a JSON serialisation library like Jackson or GSON. However exactly how they interpret the
147+
data result is particular to them. For example `nulls` are important in graphql results and hence you must set up the json mappers
148+
to include them.
149+
150+
To ensure you get a JSON result that confirms 100% to the graphql spec, you should call `toSpecification` on the result and then
151+
send that back as JSON.
152+
153+
This will ensure that the result follows the specification outlined in http://facebook.github.io/graphql/#sec-Response
154+
155+
156+
.. code-block:: java
157+
158+
ExecutionResult executionResult = graphQL.execute(executionInput);
159+
160+
Map<String, Object> toSpecificationResult = executionResult.toSpecification();
161+
162+
sendAsJson(toSpecificationResult);
163+
164+
110165
111166
Mutations
112167
---------

src/main/java/graphql/ExceptionWhileDataFetching.java

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,49 @@
55
import graphql.language.SourceLocation;
66

77
import java.util.Collections;
8+
import java.util.LinkedHashMap;
89
import java.util.List;
10+
import java.util.Map;
911

1012
import static graphql.Assert.assertNotNull;
1113
import static java.lang.String.format;
1214

1315
@PublicApi
1416
public class ExceptionWhileDataFetching implements GraphQLError {
1517

16-
private final ExecutionPath path;
18+
private final String message;
19+
private final List<Object> path;
1720
private final Throwable exception;
18-
private final SourceLocation sourceLocation;
21+
private final List<SourceLocation> locations;
22+
private final Map<String, Object> extensions;
1923

2024
public ExceptionWhileDataFetching(ExecutionPath path, Throwable exception, SourceLocation sourceLocation) {
21-
this.path = assertNotNull(path);
25+
this.path = assertNotNull(path).toList();
2226
this.exception = assertNotNull(exception);
23-
this.sourceLocation = sourceLocation;
27+
this.locations = Collections.singletonList(sourceLocation);
28+
this.extensions = mkExtensions(exception);
29+
this.message = mkMessage(path, exception);
30+
}
31+
32+
private String mkMessage(ExecutionPath path, Throwable exception) {
33+
return format("Exception while fetching data (%s) : %s", path, exception.getMessage());
34+
}
35+
36+
/*
37+
* This allows a DataFetcher to throw a graphql error and have "extension data" be transferred from that
38+
* exception into the ExceptionWhileDataFetching error and hence have custom "extension attributes"
39+
* per error message.
40+
*/
41+
private Map<String, Object> mkExtensions(Throwable exception) {
42+
Map<String, Object> extensions = null;
43+
if (exception instanceof GraphQLError) {
44+
Map<String, Object> map = ((GraphQLError) exception).getExtensions();
45+
if (map != null) {
46+
extensions = new LinkedHashMap<>();
47+
extensions.putAll(map);
48+
}
49+
}
50+
return extensions;
2451
}
2552

2653
public Throwable getException() {
@@ -30,22 +57,21 @@ public Throwable getException() {
3057

3158
@Override
3259
public String getMessage() {
33-
return format("Exception while fetching data (%s) : %s", path, exception.getMessage());
60+
return message;
3461
}
3562

3663
@Override
3764
public List<SourceLocation> getLocations() {
38-
return Collections.singletonList(sourceLocation);
65+
return locations;
3966
}
4067

41-
/**
42-
* The graphql spec says that that path field of any error should be a list
43-
* of path entries - http://facebook.github.io/graphql/#sec-Errors
44-
*
45-
* @return the path in list format
46-
*/
4768
public List<Object> getPath() {
48-
return path.toList();
69+
return path;
70+
}
71+
72+
@Override
73+
public Map<String, Object> getExtensions() {
74+
return extensions;
4975
}
5076

5177
@Override
@@ -58,18 +84,18 @@ public String toString() {
5884
return "ExceptionWhileDataFetching{" +
5985
"path=" + path +
6086
"exception=" + exception +
61-
"sourceLocation=" + sourceLocation +
87+
"locations=" + locations +
6288
'}';
6389
}
6490

6591
@SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
6692
@Override
6793
public boolean equals(Object o) {
68-
return Helper.equals(this, o);
94+
return GraphqlErrorHelper.equals(this, o);
6995
}
7096

7197
@Override
7298
public int hashCode() {
73-
return Helper.hashCode(this);
99+
return GraphqlErrorHelper.hashCode(this);
74100
}
75101
}

src/main/java/graphql/ExecutionResultImpl.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import java.util.List;
88
import java.util.Map;
99

10+
import static java.util.stream.Collectors.toList;
11+
1012
@Internal
1113
public class ExecutionResultImpl implements ExecutionResult {
1214

@@ -67,14 +69,18 @@ public Map<String, Object> toSpecification() {
6769
result.put("data", data);
6870
}
6971
if (errors != null && !errors.isEmpty()) {
70-
result.put("errors", errors);
72+
result.put("errors", errorsToSpec(errors));
7173
}
7274
if (extensions != null) {
7375
result.put("extensions", extensions);
7476
}
7577
return result;
7678
}
7779

80+
private Object errorsToSpec(List<GraphQLError> errors) {
81+
return errors.stream().map(GraphQLError::toSpecification).collect(toList());
82+
}
83+
7884
@Override
7985
public String toString() {
8086
return "ExecutionResultImpl{" +

src/main/java/graphql/GraphQLError.java

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import graphql.language.SourceLocation;
55

66
import java.util.List;
7+
import java.util.Map;
78

89
/**
910
* @see <a href="https://facebook.github.io/graphql/#sec-Errors">GraphQL Spec - 7.2.2 Errors</a>
@@ -22,37 +23,40 @@ public interface GraphQLError {
2223
*/
2324
List<SourceLocation> getLocations();
2425

26+
/**
27+
* @return an enum classifying this error
28+
*/
2529
ErrorType getErrorType();
2630

31+
/**
32+
* The graphql spec says that the (optional) path field of any error should be a list
33+
* of path entries - http://facebook.github.io/graphql/#sec-Errors
34+
*
35+
* @return the path in list format
36+
*/
37+
default List<Object> getPath() {
38+
return null;
39+
}
40+
41+
/**
42+
* The graphql specification says that result of a call should be a map that follows certain rules on what items
43+
* should be present. Certain JSON serializers may or may interpret the error to spec, so this method
44+
* is provided to produce a map that strictly follows the specification.
45+
*
46+
* See : <a href="http://facebook.github.io/graphql/#sec-Errors">http://facebook.github.io/graphql/#sec-Errors</a>
47+
*
48+
* @return a map of the error that strictly follows the specification
49+
*/
50+
default Map<String, Object> toSpecification() {
51+
return GraphqlErrorHelper.toSpecification(this);
52+
}
2753

2854
/**
29-
* This little helper allows GraphQlErrors to implement
30-
* common things (hashcode/ equals) more easily
55+
* @return a map of error extensions or null if there are none
3156
*/
32-
@SuppressWarnings("SimplifiableIfStatement")
33-
class Helper {
34-
35-
public static int hashCode(GraphQLError dis) {
36-
int result = dis.getMessage() != null ? dis.getMessage().hashCode() : 0;
37-
result = 31 * result + (dis.getLocations() != null ? dis.getLocations().hashCode() : 0);
38-
result = 31 * result + dis.getErrorType().hashCode();
39-
return result;
40-
}
41-
42-
public static boolean equals(GraphQLError dis, Object o) {
43-
if (dis == o) {
44-
return true;
45-
}
46-
if (o == null || dis.getClass() != o.getClass()) return false;
47-
48-
GraphQLError dat = (GraphQLError) o;
49-
50-
if (dis.getMessage() != null ? !dis.getMessage().equals(dat.getMessage()) : dat.getMessage() != null)
51-
return false;
52-
if (dis.getLocations() != null ? !dis.getLocations().equals(dat.getLocations()) : dat.getLocations() != null)
53-
return false;
54-
return dis.getErrorType() == dat.getErrorType();
55-
}
57+
default Map<String, Object> getExtensions() {
58+
return null;
5659
}
5760

61+
5862
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package graphql;
2+
3+
import graphql.language.SourceLocation;
4+
5+
import java.util.LinkedHashMap;
6+
import java.util.List;
7+
import java.util.Map;
8+
9+
import static java.util.stream.Collectors.toList;
10+
11+
/**
12+
* This little helper allows GraphQlErrors to implement
13+
* common things (hashcode/ equals ) and to specification more easily
14+
*/
15+
@SuppressWarnings("SimplifiableIfStatement")
16+
public class GraphqlErrorHelper {
17+
18+
public static Map<String, Object> toSpecification(GraphQLError error) {
19+
Map<String, Object> errorMap = new LinkedHashMap<>();
20+
errorMap.put("message", error.getMessage());
21+
if (error.getLocations() != null) {
22+
errorMap.put("locations", locations(error.getLocations()));
23+
}
24+
if (error.getPath() != null) {
25+
errorMap.put("path", error.getPath());
26+
}
27+
if (error.getExtensions() != null) {
28+
errorMap.put("extensions", error.getExtensions());
29+
}
30+
return errorMap;
31+
}
32+
33+
public static Object locations(List<SourceLocation> locations) {
34+
return locations.stream().map(GraphqlErrorHelper::location).collect(toList());
35+
}
36+
37+
public static Object location(SourceLocation location) {
38+
Map<String, Integer> map = new LinkedHashMap<>();
39+
map.put("line", location.getLine());
40+
map.put("column", location.getColumn());
41+
return map;
42+
}
43+
44+
public static int hashCode(GraphQLError dis) {
45+
int result = dis.getMessage() != null ? dis.getMessage().hashCode() : 0;
46+
result = 31 * result + (dis.getLocations() != null ? dis.getLocations().hashCode() : 0);
47+
result = 31 * result + (dis.getPath() != null ? dis.getPath().hashCode() : 0);
48+
result = 31 * result + dis.getErrorType().hashCode();
49+
return result;
50+
}
51+
52+
public static boolean equals(GraphQLError dis, Object o) {
53+
if (dis == o) {
54+
return true;
55+
}
56+
if (o == null || dis.getClass() != o.getClass()) return false;
57+
58+
GraphQLError dat = (GraphQLError) o;
59+
60+
if (dis.getMessage() != null ? !dis.getMessage().equals(dat.getMessage()) : dat.getMessage() != null)
61+
return false;
62+
if (dis.getLocations() != null ? !dis.getLocations().equals(dat.getLocations()) : dat.getLocations() != null)
63+
return false;
64+
if (dis.getPath() != null ? !dis.getPath().equals(dat.getPath()) : dat.getPath() != null)
65+
return false;
66+
return dis.getErrorType() == dat.getErrorType();
67+
}
68+
}

0 commit comments

Comments
 (0)