Skip to content

Commit 3214a9c

Browse files
committed
Introduce GraphQlClientException hierarchy
Refine exception handling and ensure a hierarchy of exceptions that allows differentiating between transport errors vs field errors while accessing an invalid data or field. See gh-10
1 parent eb3c8d1 commit 3214a9c

File tree

10 files changed

+448
-107
lines changed

10 files changed

+448
-107
lines changed

samples/webflux-security/src/test/java/io/spring/sample/graphql/WebFluxSecuritySampleTests.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,10 @@ void invalidCredentials() {
127127
.build()
128128
.documentName("employeesNamesAndSalaries")
129129
.executeAndVerify())
130-
.hasMessage("Invalid handshake response getStatus: 401 Unauthorized");
130+
.hasMessage(
131+
"GraphQlTransport error: Invalid handshake response getStatus: 401 Unauthorized; " +
132+
"nested exception is io.netty.handler.codec.http.websocketx.WebSocketClientHandshakeException: " +
133+
"Invalid handshake response getStatus: 401 Unauthorized");
131134
}
132135

133136
}

spring-graphql/src/main/java/org/springframework/graphql/client/DefaultClientGraphQlResponse.java

Lines changed: 48 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@
3030

3131
import org.springframework.core.ParameterizedTypeReference;
3232
import org.springframework.core.ResolvableType;
33+
import org.springframework.graphql.GraphQlRequest;
3334
import org.springframework.graphql.GraphQlResponse;
3435
import org.springframework.graphql.support.MapGraphQlResponse;
3536
import org.springframework.lang.Nullable;
36-
import org.springframework.util.Assert;
3737
import org.springframework.util.CollectionUtils;
3838
import org.springframework.util.StringUtils;
3939

@@ -46,37 +46,32 @@
4646
*/
4747
class DefaultClientGraphQlResponse extends MapGraphQlResponse implements ClientGraphQlResponse {
4848

49+
private final GraphQlRequest request;
50+
4951
private final DocumentContext jsonPathDoc;
5052

5153

52-
DefaultClientGraphQlResponse(GraphQlResponse response, Configuration jsonPathConfig) {
54+
DefaultClientGraphQlResponse(GraphQlRequest request, GraphQlResponse response, Configuration jsonPathConfig) {
5355
super(response.toMap());
56+
this.request = request;
5457
this.jsonPathDoc = JsonPath.parse(response.toMap(), jsonPathConfig);
5558
}
5659

5760

5861
@Override
59-
public <D> D toEntity(Class<D> type) {
60-
assertValidResponse();
61-
return field("").toEntity(type);
62+
public ResponseField field(String path) {
63+
path = "$.data" + (StringUtils.hasText(path) ? "." + path : "");
64+
return new DefaultField(this.request, this, path, this.jsonPathDoc, getErrors());
6265
}
6366

6467
@Override
65-
public <D> D toEntity(ParameterizedTypeReference<D> type) {
66-
assertValidResponse();
68+
public <D> D toEntity(Class<D> type) {
6769
return field("").toEntity(type);
6870
}
6971

7072
@Override
71-
public ResponseField field(String path) {
72-
path = "$.data" + (StringUtils.hasText(path) ? "." + path : "");
73-
return new DefaultField(path, this.jsonPathDoc, getErrors());
74-
}
75-
76-
private void assertValidResponse() {
77-
if (!isValid()) {
78-
throw new IllegalStateException("Path not present exception");
79-
}
73+
public <D> D toEntity(ParameterizedTypeReference<D> type) {
74+
return field("").toEntity(type);
8075
}
8176

8277

@@ -85,6 +80,10 @@ private void assertValidResponse() {
8580
*/
8681
private static class DefaultField implements ResponseField {
8782

83+
private final GraphQlRequest request;
84+
85+
private final ClientGraphQlResponse response;
86+
8887
private final String path;
8988

9089
private final DocumentContext jsonPathDoc;
@@ -93,36 +92,49 @@ private static class DefaultField implements ResponseField {
9392

9493
private final List<GraphQLError> errorsBelow;
9594

95+
private final List<GraphQLError> errorsAtOrBelow;
96+
9697
private final boolean exists;
9798

9899
@Nullable
99100
private final Object value;
100101

101-
public DefaultField(String path, DocumentContext jsonPathDoc, List<GraphQLError> errors) {
102-
Assert.notNull(path, "'path' is required");
103-
this.path = path;
102+
public DefaultField(
103+
GraphQlRequest request, ClientGraphQlResponse response,
104+
String path, DocumentContext jsonPathDoc, List<GraphQLError> errors) {
105+
106+
this.request = request;
107+
this.response = response;
108+
this.path = path ;
104109
this.jsonPathDoc = jsonPathDoc;
105110

111+
106112
List<GraphQLError> errorsAt = null;
107113
List<GraphQLError> errorsBelow = null;
114+
List<GraphQLError> errorsAtOrBelow = null;
108115

109116
for (GraphQLError error : errors) {
110117
String errorPath = toJsonPath(error);
111118
if (errorPath == null) {
112119
continue;
113120
}
114-
if (errorPath.equals(path)) {
115-
errorsAt = (errorsAt != null ? errorsAt : new ArrayList<>());
116-
errorsAt.add(error);
117-
}
118121
if (errorPath.startsWith(path)) {
119-
errorsBelow = (errorsBelow != null ? errorsBelow : new ArrayList<>());
120-
errorsBelow.add(error);
122+
if (errorPath.length() == path.length()) {
123+
errorsAt = (errorsAt != null ? errorsAt : new ArrayList<>());
124+
errorsAt.add(error);
125+
}
126+
else {
127+
errorsBelow = (errorsBelow != null ? errorsBelow : new ArrayList<>());
128+
errorsBelow.add(error);
129+
}
130+
errorsAtOrBelow = (errorsAtOrBelow != null ? errorsAtOrBelow : new ArrayList<>());
131+
errorsAtOrBelow.add(error);
121132
}
122133
}
123134

124135
this.errorsAt = (errorsAt != null ? errorsAt : Collections.emptyList());
125136
this.errorsBelow = (errorsBelow != null ? errorsBelow : Collections.emptyList());
137+
this.errorsAtOrBelow = (errorsAtOrBelow != null ? errorsAtOrBelow : Collections.emptyList());
126138

127139

128140
boolean exists = true;
@@ -183,35 +195,38 @@ public List<GraphQLError> getErrorsBelow() {
183195
return this.errorsBelow;
184196
}
185197

198+
@Override
199+
public List<GraphQLError> getErrorsAtOrBelow() {
200+
return this.errorsAtOrBelow;
201+
}
202+
186203
@Override
187204
public <D> D toEntity(Class<D> entityType) {
188-
assertValidField();
205+
assertIsValid();
189206
return this.jsonPathDoc.read(this.path, new TypeRefAdapter<>(entityType));
190207
}
191208

192209
@Override
193210
public <D> D toEntity(ParameterizedTypeReference<D> entityType) {
194-
assertValidField();
211+
assertIsValid();
195212
return this.jsonPathDoc.read(this.path, new TypeRefAdapter<>(entityType));
196213
}
197214

198215
@Override
199216
public <D> List<D> toEntityList(Class<D> elementType) {
200-
assertValidField();
217+
assertIsValid();
201218
return this.jsonPathDoc.read(this.path, new TypeRefAdapter<>(List.class, elementType));
202219
}
203220

204221
@Override
205222
public <D> List<D> toEntityList(ParameterizedTypeReference<D> elementType) {
206-
assertValidField();
223+
assertIsValid();
207224
return this.jsonPathDoc.read(this.path, new TypeRefAdapter<>(List.class, elementType));
208225
}
209226

210-
private void assertValidField() {
227+
private void assertIsValid() {
211228
if (!isValid()) {
212-
throw (CollectionUtils.isEmpty(this.errorsAt) ?
213-
new IllegalStateException("Path not present exception") :
214-
new IllegalStateException("Field error exception"));
229+
throw new FieldAccessException(this.request, this.response, this);
215230
}
216231
}
217232

spring-graphql/src/main/java/org/springframework/graphql/client/DefaultGraphQlClient.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,21 +145,33 @@ public Request variables(Map<String, Object> variables) {
145145
public Mono<ClientGraphQlResponse> execute() {
146146
return initRequest().flatMap(request ->
147147
this.transport.execute(request)
148-
.map(response -> new DefaultClientGraphQlResponse(response, this.jsonPathConfig)));
148+
.map(result ->
149+
new DefaultClientGraphQlResponse(request, result, this.jsonPathConfig))
150+
.onErrorResume(
151+
ex -> !(ex instanceof GraphQlClientException),
152+
ex -> toGraphQlTransportException(ex, request)));
149153
}
150154

151155
@Override
152156
public Flux<ClientGraphQlResponse> executeSubscription() {
153157
return initRequest().flatMapMany(request ->
154158
this.transport.executeSubscription(request)
155-
.map(response -> new DefaultClientGraphQlResponse(response, this.jsonPathConfig)));
159+
.map(result ->
160+
new DefaultClientGraphQlResponse(request, result, this.jsonPathConfig))
161+
.onErrorResume(
162+
ex -> !(ex instanceof GraphQlClientException),
163+
ex -> toGraphQlTransportException(ex, request)));
156164
}
157165

158166
private Mono<GraphQlRequest> initRequest() {
159167
return this.documentMono.map(document ->
160168
new GraphQlRequest(document, this.operationName, this.variables));
161169
}
162170

171+
private <T> Mono<T> toGraphQlTransportException(Throwable ex, GraphQlRequest request) {
172+
return Mono.error(new GraphQlTransportException(ex, request));
173+
}
174+
163175
}
164176

165177

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* Copyright 2002-2022 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.client;
18+
19+
20+
import org.springframework.graphql.GraphQlRequest;
21+
import org.springframework.graphql.GraphQlResponse;
22+
23+
/**
24+
* An exception raised on an attempt to decode data from an
25+
* {@link GraphQlResponse#isValid() invalid response} or an
26+
* {@link ResponseField#isValid() invalid field}.
27+
*
28+
* @author Rossen Stoyanchev
29+
* @since 1.0.0
30+
*/
31+
@SuppressWarnings("serial")
32+
public class FieldAccessException extends GraphQlClientException {
33+
34+
private final ClientGraphQlResponse response;
35+
36+
private final ResponseField field;
37+
38+
39+
/**
40+
* Constructor with the request and response, and the accessed field.
41+
*/
42+
public FieldAccessException(GraphQlRequest request, ClientGraphQlResponse response, ResponseField field) {
43+
super(initDefaultMessage(field), null, request);
44+
this.response = response;
45+
this.field = field;
46+
}
47+
48+
private static String initDefaultMessage(ResponseField field) {
49+
return "Invalid field '" + field.getPath() + "', errors: " + field.getErrorsAtOrBelow();
50+
}
51+
52+
53+
/**
54+
* Return the [@code GraphQlResponse} for which the error ouccrred.
55+
*/
56+
public ClientGraphQlResponse getResponse() {
57+
return this.response;
58+
}
59+
60+
/**
61+
* Return the field that needed to be accessed.
62+
*/
63+
public ResponseField getField() {
64+
return this.field;
65+
}
66+
67+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2002-2022 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.client;
18+
19+
20+
import org.springframework.core.NestedRuntimeException;
21+
import org.springframework.graphql.GraphQlRequest;
22+
import org.springframework.lang.Nullable;
23+
24+
25+
/**
26+
* Base class for exceptions from {@code GraphQlClient}.
27+
*
28+
* @author Rossen Stoyanchev
29+
* @since 1.0.0
30+
*/
31+
@SuppressWarnings("serial")
32+
public class GraphQlClientException extends NestedRuntimeException {
33+
34+
private final GraphQlRequest request;
35+
36+
37+
/**
38+
* Constructor with a message, optional cause, and the request details.
39+
*/
40+
public GraphQlClientException(String message, @Nullable Throwable cause, GraphQlRequest request) {
41+
super(message, cause);
42+
this.request = request;
43+
}
44+
45+
46+
/**
47+
* Return the request for which the error occurred.
48+
*/
49+
public GraphQlRequest getRequest() {
50+
return this.request;
51+
}
52+
53+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2002-2022 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.client;
18+
19+
20+
import org.springframework.graphql.GraphQlRequest;
21+
import org.springframework.lang.Nullable;
22+
23+
/**
24+
* Exception raised by a {@link GraphQlTransport} or used to wrap an exception
25+
* from a {@code GraphQlTransport} implementation.
26+
*
27+
* @author Rossen Stoyanchev
28+
* @since 1.0.0
29+
*/
30+
@SuppressWarnings("serial")
31+
public class GraphQlTransportException extends GraphQlClientException {
32+
33+
/**
34+
* Constructor with a default message.
35+
*/
36+
public GraphQlTransportException(@Nullable Throwable cause, GraphQlRequest request) {
37+
super("GraphQlTransport error: " + cause.getMessage(), cause, request);
38+
}
39+
40+
/**
41+
* Constructor with a given message.
42+
*/
43+
public GraphQlTransportException(String message, @Nullable Throwable cause, GraphQlRequest request) {
44+
super(message, cause, request);
45+
}
46+
47+
}

0 commit comments

Comments
 (0)