Skip to content

Commit e4880e9

Browse files
committed
Merge branch '1.0.x'
2 parents 01a69fa + 6ef9152 commit e4880e9

File tree

3 files changed

+135
-23
lines changed

3 files changed

+135
-23
lines changed

spring-graphql-docs/src/docs/asciidoc/index.adoc

Lines changed: 82 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -155,47 +155,59 @@ public class GraphQlRSocketController {
155155
[[server-interception]]
156156
=== Interception
157157

158-
Transport handlers for <<server-http>> and <<server-websocket>> delegate to a
159-
`WebGraphQlInterceptor` chain with an `ExecutionGraphQlService` at the end which calls
160-
the GraphQL Java engine. Use this to access HTTP request details and customize the
161-
`ExecutionInput` for GraphQL Java.
158+
Server transports allow intercepting requests before and after the GraphQL Java engine is
159+
called to process a request.
162160

163-
For example, to extract HTTP request values and pass them to data fetchers:
161+
162+
[[server-interception-web]]
163+
==== `WebGraphQlInterceptor`
164+
165+
<<server-http>> and <<server-websocket>> transports invoke a chain of
166+
0 or more `WebGraphQlInterceptor`, followed by an `ExecutionGraphQlService` that calls
167+
the GraphQL Java engine. `WebGraphQlInterceptor` allows an application to intercept
168+
incoming requests and do one of the following:
169+
170+
- Check HTTP request details
171+
- Customize the `graphql.ExecutionInput`
172+
- Add HTTP response headers
173+
- Customize the `graphql.ExecutionResult`
174+
175+
For example, an interceptor can pass an HTTP request header to a `DataFetcher`:
164176

165177
[source,java,indent=0,subs="verbatim,quotes"]
166178
----
167-
class HeaderInterceptor implements WebGraphQlInterceptor {
179+
class HeaderInterceptor implements WebGraphQlInterceptor { <1>
168180
169181
@Override
170182
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
171-
List<String> values = request.getHeaders().get("headerName");
183+
String value = request.getHeaders().getFirst("myHeader");
172184
request.configureExecutionInput((executionInput, builder) ->
173-
builder.graphQLContext(Collections.singletonMap("headerName", values)).build());
185+
builder.graphQLContext(Collections.singletonMap("myHeader", value)).build());
174186
return chain.next(request);
175187
}
176188
}
177189
178-
// Subsequent access from a controller
179-
180190
@Controller
181-
class MyController {
191+
class MyController { <2>
182192
183193
@QueryMapping
184194
Person person(@ContextValue String myHeader) {
185195
// ...
186196
}
187197
}
188198
----
199+
<1> Interceptor adds HTTP request header value into GraphQLContext
200+
<2> Data controller method accesses the value
189201

190-
Or reversely, add values to the `GraphQLContext` and use them to update the HTTP response:
202+
Reversely, an interceptor can access values added to the `GraphQLContext` by a controller:
191203

192204
[source,java,indent=0,subs="verbatim,quotes"]
193205
----
194206
@Controller
195207
class MyController {
196208
197209
@QueryMapping
198-
Person person(GraphQLContext context) {
210+
Person person(GraphQLContext context) { <1>
199211
context.put("cookieName", "123");
200212
}
201213
}
@@ -205,7 +217,7 @@ class MyController {
205217
class HeaderInterceptor implements WebGraphQlInterceptor {
206218
207219
@Override
208-
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
220+
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) { <2>
209221
return chain.next(request).doOnNext(response -> {
210222
String value = response.getExecutionInput().getGraphQLContext().get("cookieName");
211223
ResponseCookie cookie = ResponseCookie.from("cookieName", value).build();
@@ -214,13 +226,52 @@ class HeaderInterceptor implements WebGraphQlInterceptor {
214226
}
215227
}
216228
----
229+
<1> Controller adds value to the `GraphQLContext`
230+
<2> Interceptor uses the value to add an HTTP response header
231+
232+
`WebGraphQlHandler` can modify the `ExecutionResult`, for example, to inspect and modify
233+
request validation errors that are raised before execution begins and which cannot be
234+
handled with a `DataFetcherExceptionResolver`:
217235

218-
The `WebGraphQlInterceptor` chain can be updated through the `WebGraphQlHandler` builder,
219-
and the Boot starter uses this, see Boot's section on
236+
[source,java,indent=0,subs="verbatim,quotes"]
237+
----
238+
static class RequestErrorInterceptor implements WebGraphQlInterceptor {
239+
240+
@Override
241+
public Mono<WebGraphQlResponse> intercept(WebGraphQlRequest request, Chain chain) {
242+
return chain.next(request).map(response -> {
243+
if (response.isValid()) {
244+
return response; <1>
245+
}
246+
247+
List<GraphQLError> errors = response.getErrors().stream() <2>
248+
.map(error -> {
249+
GraphqlErrorBuilder<?> builder = GraphqlErrorBuilder.newError();
250+
// ...
251+
return builder.build();
252+
})
253+
.collect(Collectors.toList());
254+
255+
return response.transform(builder -> builder.errors(errors).build()); <3>
256+
});
257+
}
258+
}
259+
----
260+
<1> Return the same if `ExecutionResult` has a "data" key with non-null value
261+
<2> Check and transform the GraphQL errors
262+
<3> Update the `ExecutionResult` with the modified errors
263+
264+
Use `WebGraphQlHandler` to configure the `WebGraphQlInterceptor` chain. This is supported
265+
by the Boot starter, see
220266
{spring-boot-ref-docs}/web.html#web.graphql.transports.http-websocket[Web Endpoints].
221267

222-
The <<server-rsocket>> transport handler delegates to a similar `GraphQlInterceptor`
223-
chain that you can use to intercept GraphQL over RSocket requests.
268+
269+
[[server-interception-rsocket]]
270+
==== `RSocketQlInterceptor`
271+
272+
Similar to <<server-interception-web>>, an `RSocketQlInterceptor` allows intercepting
273+
GraphQL over RSocket requests before and after GraphQL Java engine execution. You can use
274+
this to customize the `graphql.ExecutionInput` and the `graphql.ExecutionResult`.
224275

225276

226277

@@ -567,6 +618,19 @@ error details.
567618
Unresolved exception are logged at ERROR level along with the `executionId` to correlate
568619
to the error sent to the client. Resolved exceptions are logged at DEBUG level.
569620

621+
[[execution-exceptions-request]]
622+
==== Request Exceptions
623+
624+
The GraphQL Java engine may run into validation or other errors when parsing the request
625+
and that in turn prevent request execution. In such cases, the response contains a
626+
"data" key with `null` and one or more request-level "errors" that are global, i.e. not
627+
having a field path.
628+
629+
`DataFetcherExceptionResolver` cannot handle such global errors because they are raised
630+
before execution begins and before any `DataFetcher` is invoked. An application can use
631+
transport level interceptors to inspect and transform errors in the `ExecutionResult`.
632+
See examples under <<server-interception-web>>.
633+
570634

571635
[[execution-exceptions-subsctiption]]
572636
==== Subscription Exceptions

spring-graphql/src/main/java/org/springframework/graphql/data/GraphQlArgumentBinder.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,10 @@ private Object wrapAsOptionalIfNecessary(@Nullable Object value, ResolvableType
188188
return (type.resolve(Object.class).equals(Optional.class) ? Optional.ofNullable(value) : value);
189189
}
190190

191-
private boolean isApproximableCollectionType(Object rawValue) {
192-
return (CollectionFactory.isApproximableCollectionType(rawValue.getClass()) ||
193-
rawValue instanceof List); // it may be SingletonList
191+
private boolean isApproximableCollectionType(@Nullable Object rawValue) {
192+
return (rawValue != null &&
193+
(CollectionFactory.isApproximableCollectionType(rawValue.getClass()) ||
194+
rawValue instanceof List)); // it may be SingletonList
194195
}
195196

196197
@SuppressWarnings({"ConstantConditions", "unchecked"})
@@ -283,12 +284,15 @@ private Object createValue(
283284
if (rawValue == null && methodParam.isOptional()) {
284285
args[i] = (paramTypes[i] == Optional.class ? Optional.empty() : null);
285286
}
286-
else if (rawValue != null && isApproximableCollectionType(rawValue)) {
287+
else if (isApproximableCollectionType(rawValue)) {
287288
ResolvableType elementType = ResolvableType.forMethodParameter(methodParam);
288289
args[i] = createCollection((Collection<Object>) rawValue, elementType, bindingResult, segments);
289290
}
290291
else if (rawValue instanceof Map) {
291-
args[i] = createValueOrNull((Map<String, Object>) rawValue, paramTypes[i], bindingResult, segments);
292+
boolean isOptional = (paramTypes[i] == Optional.class);
293+
Class<?> type = (isOptional ? methodParam.nestedIfOptional().getNestedParameterType() : paramTypes[i]);
294+
Object value = createValueOrNull((Map<String, Object>) rawValue, type, bindingResult, segments);
295+
args[i] = (isOptional ? Optional.ofNullable(value) : value);
292296
}
293297
else {
294298
args[i] = convertValue(rawValue, paramTypes[i], new TypeDescriptor(methodParam), bindingResult, segments);

spring-graphql/src/test/java/org/springframework/graphql/data/GraphQlArgumentBinderTests.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.List;
2222
import java.util.Map;
2323
import java.util.Objects;
24+
import java.util.Optional;
2425
import java.util.Set;
2526
import java.util.stream.Collectors;
2627
import java.util.stream.IntStream;
@@ -32,6 +33,7 @@
3233
import org.junit.jupiter.api.Test;
3334

3435
import org.springframework.core.ResolvableType;
36+
import org.springframework.format.support.DefaultFormattingConversionService;
3537
import org.springframework.graphql.Book;
3638
import org.springframework.validation.BindException;
3739
import org.springframework.validation.FieldError;
@@ -182,6 +184,26 @@ void primaryConstructorWithBeanArgument() throws Exception {
182184
assertThat(((PrimaryConstructorItemBean) result).getAge()).isEqualTo(30);
183185
}
184186

187+
@Test
188+
void primaryConstructorWithOptionalBeanArgument() throws Exception {
189+
190+
GraphQlArgumentBinder argumentBinder =
191+
new GraphQlArgumentBinder(new DefaultFormattingConversionService());
192+
193+
Object result = argumentBinder.bind(
194+
environment(
195+
"{\"key\":{" +
196+
"\"item\":{\"name\":\"Item name\"}," +
197+
"\"name\":\"Hello\"," +
198+
"\"age\":\"30\"}}"),
199+
"key",
200+
ResolvableType.forClass(PrimaryConstructorOptionalItemBean.class));
201+
202+
assertThat(result).isNotNull().isInstanceOf(PrimaryConstructorOptionalItemBean.class);
203+
assertThat(((PrimaryConstructorOptionalItemBean) result).getItem().get().getName()).isEqualTo("Item name");
204+
assertThat(((PrimaryConstructorOptionalItemBean) result).getName().get()).isEqualTo("Hello");
205+
}
206+
185207
@Test
186208
void primaryConstructorWithNestedBeanList() throws Exception {
187209

@@ -390,6 +412,28 @@ public List<Item> getItems() {
390412
}
391413

392414

415+
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
416+
static class PrimaryConstructorOptionalItemBean {
417+
418+
private final Optional<String> name;
419+
420+
private final Optional<Item> item;
421+
422+
public PrimaryConstructorOptionalItemBean(Optional<String> name, Optional<Item> item) {
423+
this.name = name;
424+
this.item = item;
425+
}
426+
427+
public Optional<String> getName() {
428+
return this.name;
429+
}
430+
431+
public Optional<Item> getItem() {
432+
return item;
433+
}
434+
}
435+
436+
393437
static class NoPrimaryConstructorBean {
394438

395439
NoPrimaryConstructorBean(String name) {

0 commit comments

Comments
 (0)