From 7dd9cf54ca8e7b47b2718488ee2542a569ba0478 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 19 Apr 2024 16:45:21 +0100 Subject: [PATCH] Support application/graphql for request body Closes gh-948 --- .../server/webflux/GraphQlHttpHandler.java | 20 ++++++++++++++++ .../server/webmvc/GraphQlHttpHandler.java | 23 +++++++++++++++++++ .../webflux/GraphQlHttpHandlerTests.java | 16 +++++++++++++ .../webmvc/GraphQlHttpHandlerTests.java | 13 ++++++++++- 4 files changed, 71 insertions(+), 1 deletion(-) 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 9dd32592..26ac915e 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,13 +25,16 @@ import org.apache.commons.logging.LogFactory; import reactor.core.publisher.Mono; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.io.buffer.DataBuffer; import org.springframework.graphql.server.WebGraphQlHandler; import org.springframework.graphql.server.WebGraphQlRequest; import org.springframework.graphql.server.support.SerializableGraphQlRequest; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.util.Assert; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.UnsupportedMediaTypeStatusException; /** * WebFlux.fn Handler for GraphQL over HTTP requests. @@ -73,6 +76,9 @@ public class GraphQlHttpHandler { */ public Mono handleRequest(ServerRequest serverRequest) { return serverRequest.bodyToMono(SerializableGraphQlRequest.class) + .onErrorResume( + UnsupportedMediaTypeStatusException.class, + (ex) -> applyApplicationGraphQlFallback(ex, serverRequest)) .flatMap((body) -> { WebGraphQlRequest graphQlRequest = new WebGraphQlRequest( serverRequest.uri(), serverRequest.headers().asHttpHeaders(), @@ -95,6 +101,20 @@ public class GraphQlHttpHandler { }); } + private static Mono applyApplicationGraphQlFallback( + UnsupportedMediaTypeStatusException ex, ServerRequest request) { + + // Spec requires application/json but some clients still use application/graphql + return "application/graphql".equals(request.headers().firstHeader(HttpHeaders.CONTENT_TYPE)) ? + ServerRequest.from(request) + .headers((headers) -> headers.setContentType(MediaType.APPLICATION_JSON)) + .body(request.bodyToFlux(DataBuffer.class)) + .build() + .bodyToMono(SerializableGraphQlRequest.class) + .log() : + Mono.error(ex); + } + private static MediaType selectResponseMediaType(ServerRequest serverRequest) { for (MediaType accepted : serverRequest.headers().accept()) { if (SUPPORTED_MEDIA_TYPES.contains(accepted)) { 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 393d4dc0..c9c459cb 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 @@ -35,6 +35,7 @@ import org.springframework.graphql.server.WebGraphQlHandler; import org.springframework.graphql.server.WebGraphQlRequest; import org.springframework.graphql.server.support.SerializableGraphQlRequest; import org.springframework.http.HttpCookie; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.util.AlternativeJdkIdGenerator; import org.springframework.util.Assert; @@ -146,6 +147,28 @@ public class GraphQlHttpHandler { catch (IOException ex) { throw new ServerWebInputException("I/O error while reading request body", null, ex); } + catch (HttpMediaTypeNotSupportedException ex) { + return applyApplicationGraphQlFallback(request, ex); + } + } + + private static SerializableGraphQlRequest applyApplicationGraphQlFallback( + ServerRequest request, HttpMediaTypeNotSupportedException ex) throws HttpMediaTypeNotSupportedException { + + // Spec requires application/json but some clients still use application/graphql + if ("application/graphql".equals(request.headers().firstHeader(HttpHeaders.CONTENT_TYPE))) { + try { + request = ServerRequest.from(request) + .headers((headers) -> headers.setContentType(MediaType.APPLICATION_JSON)) + .body(request.body(byte[].class)) + .build(); + return request.body(SerializableGraphQlRequest.class); + } + catch (Throwable ex2) { + // ignore + } + } + throw ex; } private static MediaType selectResponseMediaType(ServerRequest serverRequest) { 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 a21c68e9..10cc70cc 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 @@ -75,6 +75,22 @@ public class GraphQlHttpHandlerTests { .verifyComplete(); } + @Test + void shouldSupportApplicationGraphQl() throws Exception { + String document = "{greeting}"; + MockServerHttpRequest httpRequest = MockServerHttpRequest.post("/") + .contentType(MediaType.parseMediaType("application/graphql")) + .accept(MediaType.ALL) + .body(initRequestBody(document)); + + MockServerHttpResponse response = handleRequest(httpRequest, this.greetingHandler); + + assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON); + StepVerifier.create(response.getBodyAsString()) + .expectNext("{\"data\":{\"greeting\":\"Hello\"}}") + .verifyComplete(); + } + @Test void shouldProduceApplicationGraphQl() throws Exception { MockServerHttpRequest httpRequest = MockServerHttpRequest.post("/") 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 90d7f547..ecdf04ce 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 @@ -36,6 +36,7 @@ import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.graphql.GraphQlSetup; import org.springframework.graphql.server.support.SerializableGraphQlRequest; import org.springframework.http.MediaType; +import org.springframework.http.converter.ByteArrayHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.mock.web.MockHttpServletRequest; @@ -55,7 +56,7 @@ import static org.assertj.core.api.Assertions.assertThatNoException; public class GraphQlHttpHandlerTests { private static final List> MESSAGE_READERS = - Collections.singletonList(new MappingJackson2HttpMessageConverter()); + List.of(new MappingJackson2HttpMessageConverter(), new ByteArrayHttpMessageConverter()); private final GraphQlHttpHandler greetingHandler = GraphQlSetup.schemaContent("type Query { greeting: String }") .queryFetcher("greeting", (env) -> "Hello").toHttpHandler(); @@ -66,6 +67,16 @@ public class GraphQlHttpHandlerTests { MockHttpServletRequest request = createServletRequest("{ greeting }", "*/*"); MockHttpServletResponse response = handleRequest(request, this.greetingHandler); assertThat(response.getContentType()).isEqualTo(MediaType.APPLICATION_JSON_VALUE); + assertThat(response.getContentAsString()).isEqualTo("{\"data\":{\"greeting\":\"Hello\"}}"); + } + + @Test + void shouldSupportApplicationGraphQl() throws Exception { + MockHttpServletRequest request = createServletRequest("{ greeting }", "*/*"); + request.setContentType("application/graphql"); + + MockHttpServletResponse response = handleRequest(request, this.greetingHandler); + assertThat(response.getContentAsString()).isEqualTo("{\"data\":{\"greeting\":\"Hello\"}}"); } @Test