diff --git a/spring-graphql-docs/modules/ROOT/pages/transports.adoc b/spring-graphql-docs/modules/ROOT/pages/transports.adoc index ffb12a59..d48b1788 100644 --- a/spring-graphql-docs/modules/ROOT/pages/transports.adoc +++ b/spring-graphql-docs/modules/ROOT/pages/transports.adoc @@ -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. diff --git a/spring-graphql/src/main/java/org/springframework/graphql/server/webflux/AbstractGraphQlHttpHandler.java b/spring-graphql/src/main/java/org/springframework/graphql/server/webflux/AbstractGraphQlHttpHandler.java new file mode 100644 index 00000000..a7df7d23 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/server/webflux/AbstractGraphQlHttpHandler.java @@ -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 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); + } + } + + +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/server/webflux/GraphQlHttpHandler.java b/spring-graphql/src/main/java/org/springframework/graphql/server/webflux/GraphQlHttpHandler.java index 36bb5ad8..ca533420 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/server/webflux/GraphQlHttpHandler.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/server/webflux/GraphQlHttpHandler.java @@ -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 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 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()); + } }); } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/server/webflux/GraphQlSseHandler.java b/spring-graphql/src/main/java/org/springframework/graphql/server/webflux/GraphQlSseHandler.java index 334b31d9..83cae32b 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/server/webflux/GraphQlSseHandler.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/server/webflux/GraphQlSseHandler.java @@ -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>> COMPLETE_EVENT = Mono.just(ServerSentEvent.>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 handleRequest(ServerRequest serverRequest) { - Flux>> data = serverRequest.bodyToMono(SerializableGraphQlRequest.class) + Flux>> data = readRequest(serverRequest) .flatMap(body -> { WebGraphQlRequest graphQlRequest = new WebGraphQlRequest( serverRequest.uri(), serverRequest.headers().asHttpHeaders(), diff --git a/spring-graphql/src/main/java/org/springframework/graphql/server/webflux/GraphQlWebSocketHandler.java b/spring-graphql/src/main/java/org/springframework/graphql/server/webflux/GraphQlWebSocketHandler.java index 0f9a40ac..68c7778c 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/server/webflux/GraphQlWebSocketHandler.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/server/webflux/GraphQlWebSocketHandler.java @@ -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 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)); }); } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/server/webflux/HttpCodecDelegate.java b/spring-graphql/src/main/java/org/springframework/graphql/server/webflux/HttpCodecDelegate.java new file mode 100644 index 00000000..4e4b975d --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/server/webflux/HttpCodecDelegate.java @@ -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>) this.encoder) + .encodeValue(response.toMap(), DefaultDataBufferFactory.sharedInstance, RESPONSE_TYPE, MimeTypeUtils.APPLICATION_JSON, null); + } + + @SuppressWarnings("unchecked") + public Mono decode(Publisher inputStream, MediaType contentType) { + return (Mono) this.decoder.decodeToMono(inputStream, REQUEST_TYPE, contentType, null); + } + +} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/server/webflux/CodecDelegate.java b/spring-graphql/src/main/java/org/springframework/graphql/server/webflux/WebSocketCodecDelegate.java similarity index 94% rename from spring-graphql/src/main/java/org/springframework/graphql/server/webflux/CodecDelegate.java rename to spring-graphql/src/main/java/org/springframework/graphql/server/webflux/WebSocketCodecDelegate.java index bf860e1b..0643ac4b 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/server/webflux/CodecDelegate.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/server/webflux/WebSocketCodecDelegate.java @@ -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); diff --git a/spring-graphql/src/main/java/org/springframework/graphql/server/webmvc/AbstractGraphQlHttpHandler.java b/spring-graphql/src/main/java/org/springframework/graphql/server/webmvc/AbstractGraphQlHttpHandler.java index 4d0335fe..638573a3 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/server/webmvc/AbstractGraphQlHttpHandler.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/server/webmvc/AbstractGraphQlHttpHandler.java @@ -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 messageConverter; - AbstractGraphQlHttpHandler(WebGraphQlHandler graphQlHandler) { + @SuppressWarnings("unchecked") + AbstractGraphQlHttpHandler(WebGraphQlHandler graphQlHandler, @Nullable HttpMessageConverter messageConverter) { Assert.notNull(graphQlHandler, "WebGraphQlHandler is required"); this.graphQlHandler = graphQlHandler; + this.messageConverter = (HttpMessageConverter) messageConverter; } protected static MultiValueMap 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); diff --git a/spring-graphql/src/main/java/org/springframework/graphql/server/webmvc/GraphQlHttpHandler.java b/spring-graphql/src/main/java/org/springframework/graphql/server/webmvc/GraphQlHttpHandler.java index 54c796b5..9ba20349 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/server/webmvc/GraphQlHttpHandler.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/server/webmvc/GraphQlHttpHandler.java @@ -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. + *

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); + } + }; + } + } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/server/webmvc/GraphQlSseHandler.java b/spring-graphql/src/main/java/org/springframework/graphql/server/webmvc/GraphQlSseHandler.java index 0f212707..63dc2cc4 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/server/webmvc/GraphQlSseHandler.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/server/webmvc/GraphQlSseHandler.java @@ -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); } /** diff --git a/spring-graphql/src/main/resources/META-INF/native-image/org.springframework.graphql/spring-graphql/reflect-config.json b/spring-graphql/src/main/resources/META-INF/native-image/org.springframework.graphql/spring-graphql/reflect-config.json index 6bdf25d5..e9758cb6 100644 --- a/spring-graphql/src/main/resources/META-INF/native-image/org.springframework.graphql/spring-graphql/reflect-config.json +++ b/spring-graphql/src/main/resources/META-INF/native-image/org.springframework.graphql/spring-graphql/reflect-config.json @@ -21,7 +21,7 @@ "allDeclaredMethods":true, "allDeclaredConstructors":true, "condition": { - "typeReachable": "org.springframework.graphql.server.webflux.CodecDelegate" + "typeReachable": "org.springframework.graphql.server.webflux.WebSocketCodecDelegate" } }, { diff --git a/spring-graphql/src/test/java/org/springframework/graphql/server/webflux/GraphQlHttpHandlerTests.java b/spring-graphql/src/test/java/org/springframework/graphql/server/webflux/GraphQlHttpHandlerTests.java index 8240a206..4d778748 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/server/webflux/GraphQlHttpHandlerTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/server/webflux/GraphQlHttpHandlerTests.java @@ -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> messageWriters() { + return List.of(new EncoderHttpMessageWriter<>(new DataBufferEncoder())); + } + + @Override + public List viewResolvers() { + return List.of(); + } + } + } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/server/webmvc/GraphQlHttpHandlerTests.java b/spring-graphql/src/test/java/org/springframework/graphql/server/webmvc/GraphQlHttpHandlerTests.java index e98c4964..a46b653b 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/server/webmvc/GraphQlHttpHandlerTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/server/webmvc/GraphQlHttpHandlerTests.java @@ -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 {