Support application/graphql for request body

Closes gh-948
This commit is contained in:
rstoyanchev
2024-04-19 16:45:21 +01:00
parent bf9c584919
commit 7dd9cf54ca
4 changed files with 71 additions and 1 deletions

View File

@@ -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<ServerResponse> 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<SerializableGraphQlRequest> 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)) {

View File

@@ -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) {

View File

@@ -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("/")

View File

@@ -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<HttpMessageConverter<?>> 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