Skip to content

Commit 0df247f

Browse files
committed
Merge branch '1.3.x'
2 parents 6956523 + f3f5516 commit 0df247f

File tree

14 files changed

+229
-19
lines changed

14 files changed

+229
-19
lines changed

spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/AbstractGraphQlTesterBuilder.java

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@
1818

1919
import java.time.Duration;
2020
import java.util.Collections;
21+
import java.util.Map;
2122
import java.util.function.Consumer;
2223
import java.util.function.Function;
2324
import java.util.function.Predicate;
@@ -34,6 +35,7 @@
3435
import org.springframework.graphql.GraphQlResponse;
3536
import org.springframework.graphql.ResponseError;
3637
import org.springframework.graphql.client.AbstractGraphQlClientBuilder;
38+
import org.springframework.graphql.client.ClientGraphQlRequest;
3739
import org.springframework.graphql.client.GraphQlClient;
3840
import org.springframework.graphql.client.GraphQlTransport;
3941
import org.springframework.graphql.support.DocumentSource;
@@ -167,6 +169,8 @@ public Mono<GraphQlResponse> execute(GraphQlRequest request) {
167169
.document(request.getDocument())
168170
.operationName(request.getOperationName())
169171
.variables(request.getVariables())
172+
.extensions(request.getExtensions())
173+
.attributes((map) -> copyAttributes(map, request))
170174
.execute()
171175
.cast(GraphQlResponse.class);
172176
}
@@ -177,9 +181,17 @@ public Flux<GraphQlResponse> executeSubscription(GraphQlRequest request) {
177181
.document(request.getDocument())
178182
.operationName(request.getOperationName())
179183
.variables(request.getVariables())
184+
.extensions(request.getExtensions())
185+
.attributes((map) -> copyAttributes(map, request))
180186
.executeSubscription()
181187
.cast(GraphQlResponse.class);
182188
}
189+
190+
private static void copyAttributes(Map<String, Object> map, GraphQlRequest request) {
191+
if (request instanceof ClientGraphQlRequest clientGraphQlRequest) {
192+
map.putAll(clientGraphQlRequest.getAttributes());
193+
}
194+
}
183195
};
184196
}
185197

spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTester.java

+8
Original file line numberDiff line numberDiff line change
@@ -309,12 +309,15 @@ void verifyErrors() {
309309
*/
310310
private static final class DefaultResponse implements Response, Errors {
311311

312+
private final GraphQlResponse response;
313+
312314
private final ResponseDelegate delegate;
313315

314316
private DefaultResponse(
315317
GraphQlResponse response, @Nullable Predicate<ResponseError> errorFilter,
316318
Consumer<Runnable> assertDecorator, Configuration jsonPathConfig) {
317319

320+
this.response = response;
318321
this.delegate = new ResponseDelegate(response, errorFilter, assertDecorator, jsonPathConfig);
319322
}
320323

@@ -335,6 +338,11 @@ public Errors errors() {
335338
return this;
336339
}
337340

341+
@Override
342+
public GraphQlResponse returnResponse() {
343+
return this.response;
344+
}
345+
338346
@Override
339347
public Errors filter(Predicate<ResponseError> predicate) {
340348
this.delegate.filterErrors(predicate);

spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTester.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@
2525
import reactor.core.publisher.Flux;
2626

2727
import org.springframework.core.ParameterizedTypeReference;
28+
import org.springframework.graphql.GraphQlResponse;
2829
import org.springframework.graphql.ResponseError;
2930
import org.springframework.graphql.client.GraphQlTransport;
3031
import org.springframework.graphql.support.DocumentSource;
@@ -262,6 +263,13 @@ interface Response extends Traversable {
262263
*/
263264
Errors errors();
264265

266+
267+
/**
268+
* Return the underlying {@link GraphQlResponse} for direct access.
269+
* @since 1.3.5
270+
*/
271+
GraphQlResponse returnResponse();
272+
265273
}
266274

267275
/**

spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQlTesterTests.java

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@
3030
import org.springframework.graphql.ExecutionGraphQlRequest;
3131
import org.springframework.graphql.ExecutionGraphQlService;
3232
import org.springframework.graphql.GraphQlRequest;
33+
import org.springframework.graphql.GraphQlResponse;
3334

3435
import static org.assertj.core.api.Assertions.assertThat;
3536
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
@@ -500,4 +501,14 @@ void errorsConsumed() {
500501
assertThat(getActualRequestDocument()).contains(document);
501502
}
502503

504+
@Test
505+
void returnGraphQlResponse() {
506+
String document = "{me {name, friends}}";
507+
getGraphQlService().setDataAsJson(document, "{\"me\": {\"name\":\"Luke Skywalker\", \"friends\":[]}}");
508+
509+
GraphQlResponse response = graphQlTester().documentName("me").execute().returnResponse();
510+
String value = response.field("me.name").getValue();
511+
assertThat(value).isEqualTo("Luke Skywalker");
512+
}
513+
503514
}

spring-graphql/src/main/java/org/springframework/graphql/execution/ContextDataFetcherDecorator.java

+25-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import graphql.ExecutionInput;
2222
import graphql.GraphQLContext;
2323
import graphql.TrivialDataFetcher;
24+
import graphql.execution.DataFetcherResult;
2425
import graphql.schema.DataFetcher;
2526
import graphql.schema.DataFetchingEnvironment;
2627
import graphql.schema.FieldCoordinates;
@@ -39,6 +40,7 @@
3940
import reactor.core.publisher.Mono;
4041

4142
import org.springframework.graphql.ExecutionGraphQlRequest;
43+
import org.springframework.lang.Nullable;
4244
import org.springframework.util.Assert;
4345

4446
/**
@@ -74,7 +76,6 @@ private ContextDataFetcherDecorator(
7476
}
7577

7678

77-
@SuppressWarnings("ReactiveStreamsUnusedPublisher")
7879
@Override
7980
public Object get(DataFetchingEnvironment env) throws Exception {
8081

@@ -83,10 +84,33 @@ public Object get(DataFetchingEnvironment env) throws Exception {
8384
ContextSnapshot snapshot = (env.getLocalContext() instanceof GraphQLContext localContext) ?
8485
snapshotFactory.captureFrom(graphQlContext, localContext) :
8586
snapshotFactory.captureFrom(graphQlContext);
87+
8688
Mono<Void> cancelledRequest = graphQlContext.get(ExecutionGraphQlRequest.CANCEL_PUBLISHER_CONTEXT_KEY);
8789

8890
Object value = snapshot.wrap(() -> this.delegate.get(env)).call();
8991

92+
if (value instanceof DataFetcherResult<?> dataFetcherResult) {
93+
Object adapted = updateValue(dataFetcherResult.getData(), snapshot, cancelledRequest);
94+
value = DataFetcherResult.newResult()
95+
.data(adapted)
96+
.errors(dataFetcherResult.getErrors())
97+
.localContext(dataFetcherResult.getLocalContext()).build();
98+
}
99+
else {
100+
value = updateValue(value, snapshot, cancelledRequest);
101+
}
102+
103+
return value;
104+
}
105+
106+
@SuppressWarnings("ReactiveStreamsUnusedPublisher")
107+
private @Nullable Object updateValue(
108+
@Nullable Object value, ContextSnapshot snapshot, @Nullable Mono<Void> cancelledRequest) {
109+
110+
if (value == null) {
111+
return null;
112+
}
113+
90114
if (this.subscription) {
91115
Flux<?> subscriptionResult = ReactiveAdapterRegistryHelper.toSubscriptionFlux(value)
92116
.onErrorResume((exception) -> {

spring-graphql/src/main/java/org/springframework/graphql/server/webflux/AbstractGraphQlHttpHandler.java

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2024 the original author or authors.
2+
* Copyright 2020-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -31,6 +31,7 @@
3131
import org.springframework.graphql.server.WebGraphQlResponse;
3232
import org.springframework.graphql.server.support.SerializableGraphQlRequest;
3333
import org.springframework.http.HttpHeaders;
34+
import org.springframework.http.InvalidMediaTypeException;
3435
import org.springframework.http.MediaType;
3536
import org.springframework.http.codec.CodecConfigurer;
3637
import org.springframework.lang.Nullable;
@@ -102,7 +103,15 @@ public Mono<ServerResponse> handleRequest(ServerRequest request) {
102103

103104
private Mono<SerializableGraphQlRequest> readRequest(ServerRequest serverRequest) {
104105
if (this.codecDelegate != null) {
105-
MediaType contentType = serverRequest.headers().contentType().orElse(MediaType.APPLICATION_JSON);
106+
ServerRequest.Headers headers = serverRequest.headers();
107+
MediaType contentType;
108+
try {
109+
contentType = headers.contentType().orElse(MediaType.APPLICATION_OCTET_STREAM);
110+
}
111+
catch (InvalidMediaTypeException ex) {
112+
throw new UnsupportedMediaTypeStatusException("Could not parse " +
113+
"Content-Type [" + headers.firstHeader(HttpHeaders.CONTENT_TYPE) + "]: " + ex.getMessage());
114+
}
106115
return this.codecDelegate.decode(serverRequest.bodyToFlux(DataBuffer.class), contentType);
107116
}
108117
else {

spring-graphql/src/main/java/org/springframework/graphql/server/webflux/GraphQlHttpHandler.java

+13-1
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,14 @@
2323
import org.springframework.graphql.MediaTypes;
2424
import org.springframework.graphql.server.WebGraphQlHandler;
2525
import org.springframework.graphql.server.WebGraphQlResponse;
26+
import org.springframework.http.HttpHeaders;
2627
import org.springframework.http.HttpStatus;
28+
import org.springframework.http.InvalidMediaTypeException;
2729
import org.springframework.http.MediaType;
2830
import org.springframework.http.codec.CodecConfigurer;
2931
import org.springframework.web.reactive.function.server.ServerRequest;
3032
import org.springframework.web.reactive.function.server.ServerResponse;
33+
import org.springframework.web.server.NotAcceptableStatusException;
3134

3235
/**
3336
* WebFlux.fn Handler for GraphQL over HTTP requests.
@@ -111,7 +114,16 @@ protected HttpStatus selectResponseStatus(WebGraphQlResponse response, MediaType
111114
}
112115

113116
private static MediaType selectResponseMediaType(ServerRequest serverRequest) {
114-
for (MediaType accepted : serverRequest.headers().accept()) {
117+
ServerRequest.Headers headers = serverRequest.headers();
118+
List<MediaType> acceptedMediaTypes;
119+
try {
120+
acceptedMediaTypes = headers.accept();
121+
}
122+
catch (InvalidMediaTypeException ex) {
123+
throw new NotAcceptableStatusException("Could not parse " +
124+
"Accept header [" + headers.firstHeader(HttpHeaders.ACCEPT) + "]: " + ex.getMessage());
125+
}
126+
for (MediaType accepted : acceptedMediaTypes) {
115127
if (SUPPORTED_MEDIA_TYPES.contains(accepted)) {
116128
return accepted;
117129
}

spring-graphql/src/main/java/org/springframework/graphql/server/webflux/GraphQlRequestPredicates.java

+19-2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.springframework.graphql.MediaTypes;
2626
import org.springframework.http.HttpHeaders;
2727
import org.springframework.http.HttpMethod;
28+
import org.springframework.http.InvalidMediaTypeException;
2829
import org.springframework.http.MediaType;
2930
import org.springframework.http.server.PathContainer;
3031
import org.springframework.lang.Nullable;
@@ -34,6 +35,8 @@
3435
import org.springframework.web.reactive.function.server.RequestPredicate;
3536
import org.springframework.web.reactive.function.server.RouterFunctions;
3637
import org.springframework.web.reactive.function.server.ServerRequest;
38+
import org.springframework.web.server.NotAcceptableStatusException;
39+
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
3740
import org.springframework.web.util.pattern.PathPattern;
3841
import org.springframework.web.util.pattern.PathPatternParser;
3942

@@ -122,7 +125,14 @@ private static boolean contentTypeMatch(ServerRequest request, List<MediaType> c
122125
return true;
123126
}
124127
ServerRequest.Headers headers = request.headers();
125-
MediaType actual = headers.contentType().orElse(MediaType.APPLICATION_OCTET_STREAM);
128+
MediaType actual;
129+
try {
130+
actual = headers.contentType().orElse(MediaType.APPLICATION_OCTET_STREAM);
131+
}
132+
catch (InvalidMediaTypeException ex) {
133+
throw new UnsupportedMediaTypeStatusException("Could not parse " +
134+
"Content-Type [" + headers.firstHeader(HttpHeaders.CONTENT_TYPE) + "]: " + ex.getMessage());
135+
}
126136
boolean contentTypeMatch = false;
127137
for (MediaType contentType : contentTypes) {
128138
contentTypeMatch = contentType.includes(actual);
@@ -139,7 +149,14 @@ private static boolean acceptMatch(ServerRequest request, List<MediaType> expect
139149
return true;
140150
}
141151
ServerRequest.Headers headers = request.headers();
142-
List<MediaType> acceptedMediaTypes = acceptedMediaTypes(headers);
152+
List<MediaType> acceptedMediaTypes;
153+
try {
154+
acceptedMediaTypes = acceptedMediaTypes(headers);
155+
}
156+
catch (InvalidMediaTypeException ex) {
157+
throw new NotAcceptableStatusException("Could not parse " +
158+
"Accept header [" + headers.firstHeader(HttpHeaders.ACCEPT) + "]: " + ex.getMessage());
159+
}
143160
boolean match = false;
144161
outer:
145162
for (MediaType acceptedMediaType : acceptedMediaTypes) {

spring-graphql/src/main/java/org/springframework/graphql/server/webmvc/AbstractGraphQlHttpHandler.java

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2024 the original author or authors.
2+
* Copyright 2020-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -37,6 +37,7 @@
3737
import org.springframework.graphql.server.support.SerializableGraphQlRequest;
3838
import org.springframework.http.HttpCookie;
3939
import org.springframework.http.HttpHeaders;
40+
import org.springframework.http.InvalidMediaTypeException;
4041
import org.springframework.http.MediaType;
4142
import org.springframework.http.converter.HttpMessageConverter;
4243
import org.springframework.http.server.ServerHttpRequest;
@@ -52,6 +53,7 @@
5253
import org.springframework.util.StringUtils;
5354
import org.springframework.web.HttpMediaTypeNotSupportedException;
5455
import org.springframework.web.server.ServerWebInputException;
56+
import org.springframework.web.server.UnsupportedMediaTypeStatusException;
5557
import org.springframework.web.servlet.ModelAndView;
5658
import org.springframework.web.servlet.function.ServerRequest;
5759
import org.springframework.web.servlet.function.ServerResponse;
@@ -148,7 +150,15 @@ private static MultiValueMap<String, HttpCookie> initCookies(ServerRequest serve
148150
private GraphQlRequest readBody(ServerRequest request) throws ServletException {
149151
try {
150152
if (this.messageConverter != null) {
151-
MediaType contentType = request.headers().contentType().orElse(MediaType.APPLICATION_JSON);
153+
ServerRequest.Headers headers = request.headers();
154+
MediaType contentType;
155+
try {
156+
contentType = headers.contentType().orElse(MediaType.APPLICATION_OCTET_STREAM);
157+
}
158+
catch (InvalidMediaTypeException ex) {
159+
throw new UnsupportedMediaTypeStatusException("Could not parse " +
160+
"Content-Type [" + headers.firstHeader(HttpHeaders.CONTENT_TYPE) + "]: " + ex.getMessage());
161+
}
152162
if (this.messageConverter.canRead(SerializableGraphQlRequest.class, contentType)) {
153163
ServerHttpRequest httpRequest = new ServletServerHttpRequest(request.servletRequest());
154164
return (GraphQlRequest) this.messageConverter.read(SerializableGraphQlRequest.class, httpRequest);

spring-graphql/src/main/java/org/springframework/graphql/server/webmvc/GraphQlHttpHandler.java

+13-1
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,13 @@
2626
import org.springframework.graphql.MediaTypes;
2727
import org.springframework.graphql.server.WebGraphQlHandler;
2828
import org.springframework.graphql.server.WebGraphQlResponse;
29+
import org.springframework.http.HttpHeaders;
2930
import org.springframework.http.HttpStatus;
31+
import org.springframework.http.InvalidMediaTypeException;
3032
import org.springframework.http.MediaType;
3133
import org.springframework.http.converter.HttpMessageConverter;
3234
import org.springframework.lang.Nullable;
35+
import org.springframework.web.server.NotAcceptableStatusException;
3336
import org.springframework.web.servlet.function.ServerRequest;
3437
import org.springframework.web.servlet.function.ServerResponse;
3538

@@ -140,7 +143,16 @@ protected HttpStatus selectResponseStatus(WebGraphQlResponse response, MediaType
140143
}
141144

142145
private static MediaType selectResponseMediaType(ServerRequest request) {
143-
for (MediaType mediaType : request.headers().accept()) {
146+
ServerRequest.Headers headers = request.headers();
147+
List<MediaType> acceptedMediaTypes;
148+
try {
149+
acceptedMediaTypes = headers.accept();
150+
}
151+
catch (InvalidMediaTypeException ex) {
152+
throw new NotAcceptableStatusException("Could not parse " +
153+
"Accept header [" + headers.firstHeader(HttpHeaders.ACCEPT) + "]: " + ex.getMessage());
154+
}
155+
for (MediaType mediaType : acceptedMediaTypes) {
144156
if (SUPPORTED_MEDIA_TYPES.contains(mediaType)) {
145157
return mediaType;
146158
}

0 commit comments

Comments
 (0)