Configure Jackson codec specifically for GraphQL HTTP endpoints
Prior to this commit, the `GraphQlHttpHandler` implementations would use the JSON codecs configured in the web Framework (MVC or WebFlux) for reading and writing GraphQL payloads as JSON documents. This can cause issues in cases the application configures the JSON codec in a way that makes it incompatible with the expected GraphQL documents. For example, not serializing empty values and arrays. This commit adds new constructors in `GraphQlHttpHandler` implementations that can get a custom JSON codec for GraphQL payloads. Closes gh-860
This commit is contained in:
@@ -29,6 +29,11 @@ xref:boot-starter.adoc[Boot Starter] does this, see the
|
||||
details, or check `GraphQlWebMvcAutoConfiguration` or `GraphQlWebFluxAutoConfiguration`
|
||||
it contains, for the actual config.
|
||||
|
||||
By default, the `GraphQlHttpHandler` will serialize and deserialize JSON payloads using the `HttpMessageConverter` (Spring MVC)
|
||||
and the `DecoderHttpMessageReader/EncoderHttpMessageWriter` (WebFlux) configured in the web framework.
|
||||
In some cases, the application will configure the JSON codec for the HTTP endpoint in a way that is not compatible with the GraphQL payloads.
|
||||
Applications can instantiate `GraphQlHttpHandler` with a custom JSON codec that will be used for GraphQL payloads.
|
||||
|
||||
The 1.0.x branch of this repository contains a Spring MVC
|
||||
{github-10x-branch}/samples/webmvc-http[HTTP sample] application.
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2020-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.graphql.server.webflux;
|
||||
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.graphql.server.WebGraphQlHandler;
|
||||
import org.springframework.graphql.server.support.SerializableGraphQlRequest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
|
||||
/**
|
||||
* Abstract class for GraphQL Handler implementations using the HTTP transport.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 1.3.0
|
||||
*/
|
||||
class AbstractGraphQlHttpHandler {
|
||||
|
||||
protected final WebGraphQlHandler graphQlHandler;
|
||||
|
||||
@Nullable
|
||||
protected final HttpCodecDelegate codecDelegate;
|
||||
|
||||
public AbstractGraphQlHttpHandler(WebGraphQlHandler graphQlHandler, @Nullable HttpCodecDelegate codecDelegate) {
|
||||
Assert.notNull(graphQlHandler, "WebGraphQlHandler is required");
|
||||
this.graphQlHandler = graphQlHandler;
|
||||
this.codecDelegate = codecDelegate;
|
||||
}
|
||||
|
||||
protected Mono<SerializableGraphQlRequest> readRequest(ServerRequest serverRequest) {
|
||||
if (this.codecDelegate != null) {
|
||||
MediaType contentType = serverRequest.headers().contentType().orElse(MediaType.APPLICATION_JSON);
|
||||
return this.codecDelegate.decode(serverRequest.bodyToFlux(DataBuffer.class), contentType);
|
||||
}
|
||||
else {
|
||||
return serverRequest.bodyToMono(SerializableGraphQlRequest.class);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -25,9 +25,8 @@ import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.graphql.server.WebGraphQlHandler;
|
||||
import org.springframework.graphql.server.WebGraphQlRequest;
|
||||
import org.springframework.graphql.server.support.SerializableGraphQlRequest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.http.codec.CodecConfigurer;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
|
||||
@@ -38,7 +37,7 @@ import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
* @author Brian Clozel
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class GraphQlHttpHandler {
|
||||
public class GraphQlHttpHandler extends AbstractGraphQlHttpHandler {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(GraphQlHttpHandler.class);
|
||||
|
||||
@@ -46,24 +45,32 @@ public class GraphQlHttpHandler {
|
||||
private static final List<MediaType> SUPPORTED_MEDIA_TYPES =
|
||||
Arrays.asList(MediaType.APPLICATION_GRAPHQL_RESPONSE, MediaType.APPLICATION_JSON, MediaType.APPLICATION_GRAPHQL);
|
||||
|
||||
private final WebGraphQlHandler graphQlHandler;
|
||||
|
||||
/**
|
||||
* Create a new instance.
|
||||
* @param graphQlHandler common handler for GraphQL over HTTP requests
|
||||
*/
|
||||
public GraphQlHttpHandler(WebGraphQlHandler graphQlHandler) {
|
||||
Assert.notNull(graphQlHandler, "WebGraphQlHandler is required");
|
||||
this.graphQlHandler = graphQlHandler;
|
||||
super(graphQlHandler, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance.
|
||||
* @param graphQlHandler common handler for GraphQL over HTTP requests
|
||||
* @param codecConfigurer codec configurer for JSON encoding and decoding
|
||||
*/
|
||||
public GraphQlHttpHandler(WebGraphQlHandler graphQlHandler, CodecConfigurer codecConfigurer) {
|
||||
super(graphQlHandler, new HttpCodecDelegate(codecConfigurer));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle GraphQL requests over HTTP.
|
||||
* @param serverRequest the incoming HTTP request
|
||||
* @return the HTTP response
|
||||
*/
|
||||
public Mono<ServerResponse> handleRequest(ServerRequest serverRequest) {
|
||||
return serverRequest.bodyToMono(SerializableGraphQlRequest.class)
|
||||
return readRequest(serverRequest)
|
||||
.flatMap(body -> {
|
||||
WebGraphQlRequest graphQlRequest = new WebGraphQlRequest(
|
||||
serverRequest.uri(), serverRequest.headers().asHttpHeaders(),
|
||||
@@ -82,7 +89,12 @@ public class GraphQlHttpHandler {
|
||||
ServerResponse.BodyBuilder builder = ServerResponse.ok();
|
||||
builder.headers(headers -> headers.putAll(response.getResponseHeaders()));
|
||||
builder.contentType(selectResponseMediaType(serverRequest));
|
||||
return builder.bodyValue(response.toMap());
|
||||
if (this.codecDelegate != null) {
|
||||
return builder.bodyValue(this.codecDelegate.encode(response));
|
||||
}
|
||||
else {
|
||||
return builder.bodyValue(response.toMap());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -32,10 +32,8 @@ import reactor.core.publisher.Mono;
|
||||
import org.springframework.graphql.execution.SubscriptionPublisherException;
|
||||
import org.springframework.graphql.server.WebGraphQlHandler;
|
||||
import org.springframework.graphql.server.WebGraphQlRequest;
|
||||
import org.springframework.graphql.server.support.SerializableGraphQlRequest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.ServerSentEvent;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.reactive.function.BodyInserters;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
@@ -50,18 +48,15 @@ import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
* @author Brian Clozel
|
||||
* @since 1.3.0
|
||||
*/
|
||||
public class GraphQlSseHandler {
|
||||
public class GraphQlSseHandler extends AbstractGraphQlHttpHandler {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(GraphQlSseHandler.class);
|
||||
|
||||
private static final Mono<ServerSentEvent<Map<String, Object>>> COMPLETE_EVENT = Mono.just(ServerSentEvent.<Map<String, Object>>builder(Collections.emptyMap()).event("complete").build());
|
||||
|
||||
private final WebGraphQlHandler graphQlHandler;
|
||||
|
||||
|
||||
public GraphQlSseHandler(WebGraphQlHandler graphQlHandler) {
|
||||
Assert.notNull(graphQlHandler, "WebGraphQlHandler is required");
|
||||
this.graphQlHandler = graphQlHandler;
|
||||
super(graphQlHandler, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,7 +67,7 @@ public class GraphQlSseHandler {
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public Mono<ServerResponse> handleRequest(ServerRequest serverRequest) {
|
||||
Flux<ServerSentEvent<Map<String, Object>>> data = serverRequest.bodyToMono(SerializableGraphQlRequest.class)
|
||||
Flux<ServerSentEvent<Map<String, Object>>> data = readRequest(serverRequest)
|
||||
.flatMap(body -> {
|
||||
WebGraphQlRequest graphQlRequest = new WebGraphQlRequest(
|
||||
serverRequest.uri(), serverRequest.headers().asHttpHeaders(),
|
||||
|
||||
@@ -72,7 +72,7 @@ public class GraphQlWebSocketHandler implements WebSocketHandler {
|
||||
|
||||
private final WebSocketGraphQlInterceptor webSocketInterceptor;
|
||||
|
||||
private final CodecDelegate codecDelegate;
|
||||
private final WebSocketCodecDelegate webSocketCodecDelegate;
|
||||
|
||||
private final Duration initTimeoutDuration;
|
||||
|
||||
@@ -91,7 +91,7 @@ public class GraphQlWebSocketHandler implements WebSocketHandler {
|
||||
|
||||
this.graphQlHandler = graphQlHandler;
|
||||
this.webSocketInterceptor = this.graphQlHandler.getWebSocketInterceptor();
|
||||
this.codecDelegate = new CodecDelegate(codecConfigurer);
|
||||
this.webSocketCodecDelegate = new WebSocketCodecDelegate(codecConfigurer);
|
||||
this.initTimeoutDuration = connectionInitTimeout;
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ public class GraphQlWebSocketHandler implements WebSocketHandler {
|
||||
.subscribe();
|
||||
|
||||
return session.send(session.receive().flatMap(webSocketMessage -> {
|
||||
GraphQlWebSocketMessage message = this.codecDelegate.decode(webSocketMessage);
|
||||
GraphQlWebSocketMessage message = this.webSocketCodecDelegate.decode(webSocketMessage);
|
||||
String id = message.getId();
|
||||
Map<String, Object> payload = message.getPayload();
|
||||
switch (message.resolvedType()) {
|
||||
@@ -159,7 +159,7 @@ public class GraphQlWebSocketHandler implements WebSocketHandler {
|
||||
.doOnTerminate(() -> subscriptions.remove(id));
|
||||
}
|
||||
case PING -> {
|
||||
return Flux.just(this.codecDelegate.encode(session, GraphQlWebSocketMessage.pong(null)));
|
||||
return Flux.just(this.webSocketCodecDelegate.encode(session, GraphQlWebSocketMessage.pong(null)));
|
||||
}
|
||||
case COMPLETE -> {
|
||||
if (id != null) {
|
||||
@@ -178,7 +178,7 @@ public class GraphQlWebSocketHandler implements WebSocketHandler {
|
||||
}
|
||||
return this.webSocketInterceptor.handleConnectionInitialization(sessionInfo, payload)
|
||||
.defaultIfEmpty(Collections.emptyMap())
|
||||
.map(ackPayload -> this.codecDelegate.encodeConnectionAck(session, ackPayload))
|
||||
.map(ackPayload -> this.webSocketCodecDelegate.encodeConnectionAck(session, ackPayload))
|
||||
.flux()
|
||||
.onErrorResume(ex -> GraphQlStatus.close(session, GraphQlStatus.UNAUTHORIZED_STATUS));
|
||||
}
|
||||
@@ -218,14 +218,14 @@ public class GraphQlWebSocketHandler implements WebSocketHandler {
|
||||
}
|
||||
|
||||
return responseFlux
|
||||
.map(responseMap -> this.codecDelegate.encodeNext(session, id, responseMap))
|
||||
.concatWith(Mono.fromCallable(() -> this.codecDelegate.encodeComplete(session, id)))
|
||||
.map(responseMap -> this.webSocketCodecDelegate.encodeNext(session, id, responseMap))
|
||||
.concatWith(Mono.fromCallable(() -> this.webSocketCodecDelegate.encodeComplete(session, id)))
|
||||
.onErrorResume(ex -> {
|
||||
if (ex instanceof SubscriptionExistsException) {
|
||||
CloseStatus status = new CloseStatus(4409, "Subscriber for " + id + " already exists");
|
||||
return GraphQlStatus.close(session, status);
|
||||
}
|
||||
return Mono.fromCallable(() -> this.codecDelegate.encodeError(session, id, ex));
|
||||
return Mono.fromCallable(() -> this.webSocketCodecDelegate.encodeError(session, id, ex));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.graphql.server.webflux;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.codec.Decoder;
|
||||
import org.springframework.core.codec.Encoder;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import org.springframework.graphql.GraphQlResponse;
|
||||
import org.springframework.graphql.server.support.SerializableGraphQlRequest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.CodecConfigurer;
|
||||
import org.springframework.http.codec.DecoderHttpMessageReader;
|
||||
import org.springframework.http.codec.EncoderHttpMessageWriter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
|
||||
/**
|
||||
* Helper class for encoding and decoding GraphQL messages in HTTP transport.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 1.3.0
|
||||
*/
|
||||
final class HttpCodecDelegate {
|
||||
|
||||
private static final ResolvableType REQUEST_TYPE = ResolvableType.forClass(SerializableGraphQlRequest.class);
|
||||
|
||||
private static final ResolvableType RESPONSE_TYPE = ResolvableType.forClassWithGenerics(Map.class, String.class, Object.class);
|
||||
|
||||
|
||||
private final Decoder<?> decoder;
|
||||
|
||||
private final Encoder<?> encoder;
|
||||
|
||||
|
||||
HttpCodecDelegate(CodecConfigurer codecConfigurer) {
|
||||
Assert.notNull(codecConfigurer, "CodecConfigurer is required");
|
||||
this.decoder = findJsonDecoder(codecConfigurer);
|
||||
this.encoder = findJsonEncoder(codecConfigurer);
|
||||
}
|
||||
|
||||
private static Decoder<?> findJsonDecoder(CodecConfigurer configurer) {
|
||||
return configurer.getReaders().stream()
|
||||
.filter((reader) -> reader.canRead(REQUEST_TYPE, MediaType.APPLICATION_JSON))
|
||||
.map((reader) -> ((DecoderHttpMessageReader<?>) reader).getDecoder())
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("No JSON Decoder"));
|
||||
}
|
||||
|
||||
private static Encoder<?> findJsonEncoder(CodecConfigurer configurer) {
|
||||
return configurer.getWriters().stream()
|
||||
.filter((writer) -> writer.canWrite(RESPONSE_TYPE, MediaType.APPLICATION_JSON))
|
||||
.map((writer) -> ((EncoderHttpMessageWriter<?>) writer).getEncoder())
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("No JSON Encoder"));
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public DataBuffer encode(GraphQlResponse response) {
|
||||
return ((Encoder<Map<String, Object>>) this.encoder)
|
||||
.encodeValue(response.toMap(), DefaultDataBufferFactory.sharedInstance, RESPONSE_TYPE, MimeTypeUtils.APPLICATION_JSON, null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Mono<SerializableGraphQlRequest> decode(Publisher<DataBuffer> inputStream, MediaType contentType) {
|
||||
return (Mono<SerializableGraphQlRequest>) this.decoder.decodeToMono(inputStream, REQUEST_TYPE, contentType, null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -40,12 +40,12 @@ import org.springframework.web.reactive.socket.WebSocketMessage;
|
||||
import org.springframework.web.reactive.socket.WebSocketSession;
|
||||
|
||||
/**
|
||||
* Helper class for encoding and decoding GraphQL messages.
|
||||
* Helper class for encoding and decoding GraphQL messages in WebSocket transport.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
final class CodecDelegate {
|
||||
final class WebSocketCodecDelegate {
|
||||
|
||||
private static final ResolvableType MESSAGE_TYPE = ResolvableType.forClass(GraphQlWebSocketMessage.class);
|
||||
|
||||
@@ -55,7 +55,7 @@ final class CodecDelegate {
|
||||
private final Encoder<?> encoder;
|
||||
|
||||
|
||||
CodecDelegate(CodecConfigurer codecConfigurer) {
|
||||
WebSocketCodecDelegate(CodecConfigurer codecConfigurer) {
|
||||
Assert.notNull(codecConfigurer, "CodecConfigurer is required");
|
||||
this.decoder = findJsonDecoder(codecConfigurer);
|
||||
this.encoder = findJsonEncoder(codecConfigurer);
|
||||
@@ -20,6 +20,7 @@ import java.io.IOException;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.Cookie;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
@@ -27,11 +28,16 @@ import org.springframework.graphql.GraphQlRequest;
|
||||
import org.springframework.graphql.server.WebGraphQlHandler;
|
||||
import org.springframework.graphql.server.support.SerializableGraphQlRequest;
|
||||
import org.springframework.http.HttpCookie;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.AlternativeJdkIdGenerator;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.IdGenerator;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import org.springframework.web.servlet.function.ServerRequest;
|
||||
|
||||
@@ -49,10 +55,14 @@ abstract class AbstractGraphQlHttpHandler {
|
||||
|
||||
protected final WebGraphQlHandler graphQlHandler;
|
||||
|
||||
@Nullable
|
||||
protected final HttpMessageConverter<Object> messageConverter;
|
||||
|
||||
AbstractGraphQlHttpHandler(WebGraphQlHandler graphQlHandler) {
|
||||
@SuppressWarnings("unchecked")
|
||||
AbstractGraphQlHttpHandler(WebGraphQlHandler graphQlHandler, @Nullable HttpMessageConverter<?> messageConverter) {
|
||||
Assert.notNull(graphQlHandler, "WebGraphQlHandler is required");
|
||||
this.graphQlHandler = graphQlHandler;
|
||||
this.messageConverter = (HttpMessageConverter<Object>) messageConverter;
|
||||
}
|
||||
|
||||
protected static MultiValueMap<String, HttpCookie> initCookies(ServerRequest serverRequest) {
|
||||
@@ -65,9 +75,19 @@ abstract class AbstractGraphQlHttpHandler {
|
||||
return target;
|
||||
}
|
||||
|
||||
protected static GraphQlRequest readBody(ServerRequest request) throws ServletException {
|
||||
protected GraphQlRequest readBody(ServerRequest request) throws ServletException {
|
||||
try {
|
||||
return request.body(SerializableGraphQlRequest.class);
|
||||
if (this.messageConverter != null) {
|
||||
MediaType contentType = request.headers().contentType().orElse(MediaType.APPLICATION_JSON);
|
||||
if (this.messageConverter.canRead(SerializableGraphQlRequest.class, contentType)) {
|
||||
return (GraphQlRequest) this.messageConverter.read(SerializableGraphQlRequest.class,
|
||||
new ServletServerHttpRequest(request.servletRequest()));
|
||||
}
|
||||
throw new HttpMediaTypeNotSupportedException(contentType, this.messageConverter.getSupportedMediaTypes(), request.method());
|
||||
}
|
||||
else {
|
||||
return request.body(SerializableGraphQlRequest.class);
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new ServerWebInputException("I/O error while reading request body", null, ex);
|
||||
|
||||
@@ -24,9 +24,14 @@ import java.util.concurrent.ExecutionException;
|
||||
import jakarta.servlet.ServletException;
|
||||
|
||||
import org.springframework.context.i18n.LocaleContextHolder;
|
||||
import org.springframework.graphql.GraphQlResponse;
|
||||
import org.springframework.graphql.server.WebGraphQlHandler;
|
||||
import org.springframework.graphql.server.WebGraphQlRequest;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServletServerHttpResponse;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
||||
import org.springframework.web.servlet.function.ServerRequest;
|
||||
import org.springframework.web.servlet.function.ServerResponse;
|
||||
@@ -51,7 +56,18 @@ public class GraphQlHttpHandler extends AbstractGraphQlHttpHandler{
|
||||
* @param graphQlHandler common handler for GraphQL over HTTP requests
|
||||
*/
|
||||
public GraphQlHttpHandler(WebGraphQlHandler graphQlHandler) {
|
||||
super(graphQlHandler);
|
||||
super(graphQlHandler, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance with a custom message converter.
|
||||
* <p>If no converter is provided, this will use
|
||||
* {@link org.springframework.web.servlet.config.annotation.WebMvcConfigurer#configureMessageConverters(List) the one configured in the web framework}.
|
||||
* @param graphQlHandler common handler for GraphQL over HTTP requests
|
||||
* @param messageConverter custom {@link HttpMessageConverter} to be used for encoding and decoding GraphQL payloads
|
||||
*/
|
||||
public GraphQlHttpHandler(WebGraphQlHandler graphQlHandler, @Nullable HttpMessageConverter<?> messageConverter) {
|
||||
super(graphQlHandler, messageConverter);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,10 +93,17 @@ public class GraphQlHttpHandler extends AbstractGraphQlHttpHandler{
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Execution complete");
|
||||
}
|
||||
MediaType contentType = selectResponseMediaType(serverRequest);
|
||||
ServerResponse.BodyBuilder builder = ServerResponse.ok();
|
||||
builder.headers(headers -> headers.putAll(response.getResponseHeaders()));
|
||||
builder.contentType(selectResponseMediaType(serverRequest));
|
||||
return builder.body(response.toMap());
|
||||
builder.contentType(contentType);
|
||||
|
||||
if (this.messageConverter != null) {
|
||||
return builder.build(writeFunction(contentType, response));
|
||||
}
|
||||
else {
|
||||
return builder.body(response.toMap());
|
||||
}
|
||||
})
|
||||
.toFuture();
|
||||
|
||||
@@ -108,4 +131,17 @@ public class GraphQlHttpHandler extends AbstractGraphQlHttpHandler{
|
||||
return MediaType.APPLICATION_JSON;
|
||||
}
|
||||
|
||||
private ServerResponse.HeadersBuilder.WriteFunction writeFunction(MediaType contentType, GraphQlResponse response) {
|
||||
return (servletRequest, servletResponse) -> {
|
||||
if (messageConverter != null) {
|
||||
ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(servletResponse);
|
||||
messageConverter.write(response.toMap(), contentType, httpResponse);
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
throw new HttpMediaTypeNotSupportedException(contentType, SUPPORTED_MEDIA_TYPES, HttpMethod.POST);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,10 +20,11 @@ import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
|
||||
import graphql.ErrorType;
|
||||
import graphql.ExecutionResult;
|
||||
import graphql.GraphQLError;
|
||||
import jakarta.servlet.ServletException;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.BaseSubscriber;
|
||||
import reactor.core.publisher.Flux;
|
||||
@@ -55,7 +56,7 @@ public class GraphQlSseHandler extends AbstractGraphQlHttpHandler {
|
||||
|
||||
|
||||
public GraphQlSseHandler(WebGraphQlHandler graphQlHandler) {
|
||||
super(graphQlHandler);
|
||||
super(graphQlHandler, null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"allDeclaredMethods":true,
|
||||
"allDeclaredConstructors":true,
|
||||
"condition": {
|
||||
"typeReachable": "org.springframework.graphql.server.webflux.CodecDelegate"
|
||||
"typeReachable": "org.springframework.graphql.server.webflux.WebSocketCodecDelegate"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -15,21 +15,22 @@
|
||||
*/
|
||||
package org.springframework.graphql.server.webflux;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.jayway.jsonpath.DocumentContext;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.codec.DataBufferEncoder;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import org.springframework.graphql.GraphQlRequest;
|
||||
import org.springframework.graphql.GraphQlSetup;
|
||||
import org.springframework.graphql.server.WebGraphQlHandler;
|
||||
import org.springframework.graphql.server.support.SerializableGraphQlRequest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.CodecConfigurer;
|
||||
import org.springframework.http.codec.EncoderHttpMessageWriter;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
import org.springframework.http.codec.ServerCodecConfigurer;
|
||||
import org.springframework.http.codec.json.Jackson2JsonDecoder;
|
||||
import org.springframework.http.codec.json.Jackson2JsonEncoder;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpResponse;
|
||||
@@ -38,6 +39,13 @@ import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.reactive.result.view.ViewResolver;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -119,6 +127,37 @@ public class GraphQlHttpHandlerTests {
|
||||
assertThat(id).isEqualTo(httpRequest.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseCustomCodec() {
|
||||
WebGraphQlHandler webGraphQlHandler = GraphQlSetup.schemaContent("type Query { showId: String }")
|
||||
.queryFetcher("showId", (env) -> env.getExecutionId().toString())
|
||||
.toWebGraphQlHandler();
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
CodecConfigurer configurer = ServerCodecConfigurer.create();
|
||||
configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(mapper));
|
||||
configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(mapper));
|
||||
GraphQlHttpHandler httpHandler = new GraphQlHttpHandler(webGraphQlHandler, configurer);
|
||||
|
||||
MockServerHttpRequest httpRequest = MockServerHttpRequest.post("/")
|
||||
.contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_GRAPHQL_RESPONSE).build();
|
||||
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(httpRequest);
|
||||
MockServerRequest serverRequest = MockServerRequest.builder()
|
||||
.exchange(exchange)
|
||||
.uri(((ServerWebExchange) exchange).getRequest().getURI())
|
||||
.method(((ServerWebExchange) exchange).getRequest().getMethod())
|
||||
.headers(((ServerWebExchange) exchange).getRequest().getHeaders())
|
||||
.body(Flux.just(DefaultDataBufferFactory.sharedInstance.wrap("{\"query\":\"{showId}\"}".getBytes(StandardCharsets.UTF_8))));
|
||||
|
||||
httpHandler.handleRequest(serverRequest)
|
||||
.flatMap(response -> response.writeTo(exchange, new EmptyContext()))
|
||||
.block();
|
||||
|
||||
DocumentContext document = JsonPath.parse(exchange.getResponse().getBodyAsString().block());
|
||||
String id = document.read("data.showId", String.class);
|
||||
assertThat(id).isEqualTo(httpRequest.getId());
|
||||
}
|
||||
|
||||
private static SerializableGraphQlRequest initRequest(String document) {
|
||||
SerializableGraphQlRequest request = new SerializableGraphQlRequest();
|
||||
request.setQuery(document);
|
||||
@@ -159,4 +198,16 @@ public class GraphQlHttpHandlerTests {
|
||||
|
||||
}
|
||||
|
||||
private static class EmptyContext implements ServerResponse.Context {
|
||||
@Override
|
||||
public List<HttpMessageWriter<?>> messageWriters() {
|
||||
return List.of(new EncoderHttpMessageWriter<>(new DataBufferEncoder()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ViewResolver> viewResolvers() {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.context.i18n.LocaleContextHolder;
|
||||
import org.springframework.graphql.GraphQlSetup;
|
||||
import org.springframework.graphql.server.WebGraphQlHandler;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
@@ -110,6 +111,25 @@ public class GraphQlHttpHandlerTests {
|
||||
assertThatNoException().isThrownBy(() -> UUID.fromString(id));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseCustomMessageConverter() throws Exception {
|
||||
WebGraphQlHandler webGraphQlHandler = GraphQlSetup.schemaContent("type Query { greeting: String }")
|
||||
.queryFetcher("greeting", (env) -> "Hello").toWebGraphQlHandler();
|
||||
GraphQlHttpHandler handler = new GraphQlHttpHandler(webGraphQlHandler, new MappingJackson2HttpMessageConverter());
|
||||
MockHttpServletRequest servletRequest = createServletRequest("{\"query\":\"{ greeting }\"}", MediaType.APPLICATION_GRAPHQL_RESPONSE_VALUE);
|
||||
|
||||
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
|
||||
ServerResponse response = handler.handleRequest(request);
|
||||
if (response instanceof AsyncServerResponse asyncResponse) {
|
||||
asyncResponse.block();
|
||||
}
|
||||
MockHttpServletResponse servletResponse = new MockHttpServletResponse();
|
||||
response.writeTo(servletRequest, servletResponse, new DefaultContext());
|
||||
|
||||
assertThat(servletResponse.getContentAsString())
|
||||
.isEqualTo("{\"data\":{\"greeting\":\"Hello\"}}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void persistedQuery() throws Exception {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user