Support application/graphql+json content type
The GraphQL HTTP spec now requires the `"application/graphql+json"` content type. This commit applies this type by default in the server and client implementations. `"application/json"` is still accepted and produced if requested by clients. Closes gh-108
This commit is contained in:
@@ -59,7 +59,7 @@ configure(moduleProjects) {
|
||||
imports {
|
||||
mavenBom "com.fasterxml.jackson:jackson-bom:2.13.1"
|
||||
mavenBom "io.projectreactor:reactor-bom:2020.0.17"
|
||||
mavenBom "org.springframework:spring-framework-bom:5.3.17"
|
||||
mavenBom "org.springframework:spring-framework-bom:5.3.19-SNAPSHOT"
|
||||
mavenBom "org.springframework.data:spring-data-bom:2021.2.0-M4"
|
||||
mavenBom "org.springframework.security:spring-security-bom:5.7.0-M3"
|
||||
mavenBom "com.querydsl:querydsl-bom:5.0.0"
|
||||
|
||||
@@ -56,7 +56,9 @@ request body, as defined in the proposed
|
||||
https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md[GraphQL over HTTP]
|
||||
specification. Once the JSON body has been successfully decoded, the HTTP response
|
||||
status is always 200 (OK), and any errors from GraphQL request execution appear in the
|
||||
"errors" section of the GraphQL response.
|
||||
"errors" section of the GraphQL response. The default and preferred choice of media type is
|
||||
`"application/graphql+json"`, but `"application/json"` is also supported, as described in the
|
||||
specification.
|
||||
|
||||
`GraphQlHttpHandler` can be exposed as an HTTP endpoint by declaring a `RouterFunction`
|
||||
bean and using the `RouterFunctions` from Spring MVC or WebFlux to create the route. The
|
||||
|
||||
@@ -55,12 +55,12 @@ final class WebTestClientTransport implements GraphQlTransport {
|
||||
public Mono<GraphQlResponse> execute(GraphQlRequest request) {
|
||||
|
||||
Map<String, Object> responseMap = this.webTestClient.post()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.contentType(MediaType.APPLICATION_GRAPHQL)
|
||||
.accept(MediaType.APPLICATION_GRAPHQL)
|
||||
.bodyValue(request.toMap())
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectHeader().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)
|
||||
.expectHeader().contentTypeCompatibleWith(MediaType.APPLICATION_GRAPHQL)
|
||||
.expectBody(MAP_TYPE)
|
||||
.returnResult()
|
||||
.getResponseBody();
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.springframework.messaging.rsocket.RSocketRequester;
|
||||
import org.springframework.messaging.rsocket.RSocketStrategies;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
|
||||
|
||||
/**
|
||||
@@ -60,8 +61,7 @@ final class DefaultRSocketGraphQlClientBuilder
|
||||
}
|
||||
|
||||
private static RSocketRequester.Builder initRSocketRequestBuilder() {
|
||||
MimeType mimeType = MimeType.valueOf("application/graphql+json");
|
||||
RSocketRequester.Builder requesterBuilder = RSocketRequester.builder().dataMimeType(mimeType);
|
||||
RSocketRequester.Builder requesterBuilder = RSocketRequester.builder().dataMimeType(MimeTypeUtils.APPLICATION_GRAPHQL);
|
||||
if (jackson2Present) {
|
||||
requesterBuilder.rsocketStrategies(
|
||||
RSocketStrategies.builder()
|
||||
|
||||
@@ -56,8 +56,8 @@ final class HttpGraphQlTransport implements GraphQlTransport {
|
||||
@Override
|
||||
public Mono<GraphQlResponse> execute(GraphQlRequest request) {
|
||||
return this.webClient.post()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.contentType(MediaType.APPLICATION_GRAPHQL)
|
||||
.accept(MediaType.APPLICATION_GRAPHQL, MediaType.APPLICATION_JSON)
|
||||
.bodyValue(request.toMap())
|
||||
.retrieve()
|
||||
.bodyToMono(MAP_TYPE)
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.graphql.server.webflux;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
@@ -25,6 +27,7 @@ import reactor.core.publisher.Mono;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.graphql.server.WebGraphQlHandler;
|
||||
import org.springframework.graphql.server.WebGraphQlRequest;
|
||||
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;
|
||||
@@ -44,6 +47,8 @@ public class GraphQlHttpHandler {
|
||||
new ParameterizedTypeReference<Map<String, Object>>() {
|
||||
};
|
||||
|
||||
private static final List<MediaType> SUPPORTED_MEDIA_TYPES = Arrays.asList(MediaType.APPLICATION_GRAPHQL, MediaType.APPLICATION_JSON);
|
||||
|
||||
private final WebGraphQlHandler graphQlHandler;
|
||||
|
||||
/**
|
||||
@@ -78,8 +83,18 @@ public class GraphQlHttpHandler {
|
||||
}
|
||||
ServerResponse.BodyBuilder builder = ServerResponse.ok();
|
||||
builder.headers(headers -> headers.putAll(response.getResponseHeaders()));
|
||||
builder.contentType(selectResponseMediaType(serverRequest));
|
||||
return builder.bodyValue(response.toMap());
|
||||
});
|
||||
}
|
||||
|
||||
private static MediaType selectResponseMediaType(ServerRequest serverRequest) {
|
||||
for (MediaType accepted : serverRequest.headers().accept()) {
|
||||
if (SUPPORTED_MEDIA_TYPES.contains(accepted)) {
|
||||
return accepted;
|
||||
}
|
||||
}
|
||||
return MediaType.APPLICATION_GRAPHQL;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
package org.springframework.graphql.server.webmvc;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
@@ -29,6 +31,7 @@ import org.springframework.context.i18n.LocaleContextHolder;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.graphql.server.WebGraphQlHandler;
|
||||
import org.springframework.graphql.server.WebGraphQlRequest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.AlternativeJdkIdGenerator;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.IdGenerator;
|
||||
@@ -50,7 +53,10 @@ public class GraphQlHttpHandler {
|
||||
private static final Log logger = LogFactory.getLog(GraphQlHttpHandler.class);
|
||||
|
||||
private static final ParameterizedTypeReference<Map<String, Object>> MAP_PARAMETERIZED_TYPE_REF =
|
||||
new ParameterizedTypeReference<Map<String, Object>>() {};
|
||||
new ParameterizedTypeReference<Map<String, Object>>() {
|
||||
};
|
||||
|
||||
private static final List<MediaType> SUPPORTED_MEDIA_TYPES = Arrays.asList(MediaType.APPLICATION_GRAPHQL, MediaType.APPLICATION_JSON);
|
||||
|
||||
private final IdGenerator idGenerator = new AlternativeJdkIdGenerator();
|
||||
|
||||
@@ -89,6 +95,7 @@ public class GraphQlHttpHandler {
|
||||
}
|
||||
ServerResponse.BodyBuilder builder = ServerResponse.ok();
|
||||
builder.headers(headers -> headers.putAll(response.getResponseHeaders()));
|
||||
builder.contentType(selectResponseMediaType(serverRequest));
|
||||
return builder.body(response.toMap());
|
||||
});
|
||||
|
||||
@@ -104,4 +111,13 @@ public class GraphQlHttpHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private static MediaType selectResponseMediaType(ServerRequest serverRequest) {
|
||||
for (MediaType accepted : serverRequest.headers().accept()) {
|
||||
if (SUPPORTED_MEDIA_TYPES.contains(accepted)) {
|
||||
return accepted;
|
||||
}
|
||||
}
|
||||
return MediaType.APPLICATION_GRAPHQL;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* 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.
|
||||
@@ -26,6 +26,7 @@ import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.graphql.GraphQlSetup;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.EncoderHttpMessageWriter;
|
||||
import org.springframework.http.codec.HttpMessageWriter;
|
||||
import org.springframework.http.codec.json.Jackson2JsonEncoder;
|
||||
@@ -45,14 +46,51 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
*/
|
||||
public class GraphQlHttpHandlerTests {
|
||||
|
||||
private final GraphQlHttpHandler greetingHandler = GraphQlSetup.schemaContent("type Query { greeting: String }")
|
||||
.queryFetcher("greeting", (env) -> "Hello").toHttpHandlerWebFlux();
|
||||
|
||||
|
||||
@Test
|
||||
void shouldProduceApplicationGraphQlByDefault() {
|
||||
MockServerHttpRequest httpRequest = MockServerHttpRequest.post("/")
|
||||
.contentType(MediaType.APPLICATION_GRAPHQL).accept(MediaType.ALL).build();
|
||||
|
||||
MockServerHttpResponse httpResponse = handleRequest(
|
||||
httpRequest, this.greetingHandler, Collections.singletonMap("query", "{greeting}"));
|
||||
|
||||
assertThat(httpResponse.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_GRAPHQL);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldProduceApplicationGraphQl() {
|
||||
MockServerHttpRequest httpRequest = MockServerHttpRequest.post("/")
|
||||
.contentType(MediaType.APPLICATION_GRAPHQL).accept(MediaType.APPLICATION_GRAPHQL).build();
|
||||
|
||||
MockServerHttpResponse httpResponse = handleRequest(
|
||||
httpRequest, this.greetingHandler, Collections.singletonMap("query", "{greeting}"));
|
||||
|
||||
assertThat(httpResponse.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_GRAPHQL);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldProduceApplicationJson() {
|
||||
MockServerHttpRequest httpRequest = MockServerHttpRequest.post("/")
|
||||
.contentType(MediaType.APPLICATION_GRAPHQL).accept(MediaType.APPLICATION_JSON).build();
|
||||
|
||||
MockServerHttpResponse httpResponse = handleRequest(
|
||||
httpRequest, this.greetingHandler, Collections.singletonMap("query", "{greeting}"));
|
||||
|
||||
assertThat(httpResponse.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
|
||||
}
|
||||
|
||||
@Test
|
||||
void locale() {
|
||||
GraphQlHttpHandler handler = GraphQlSetup.schemaContent("type Query { greeting: String }")
|
||||
.queryFetcher("greeting", (env) -> "Hello in " + env.getLocale())
|
||||
.toHttpHandlerWebFlux();
|
||||
|
||||
MockServerHttpRequest httpRequest =
|
||||
MockServerHttpRequest.post("/").acceptLanguageAsLocales(Locale.FRENCH).build();
|
||||
MockServerHttpRequest httpRequest = MockServerHttpRequest.post("/")
|
||||
.contentType(MediaType.APPLICATION_GRAPHQL).accept(MediaType.APPLICATION_GRAPHQL).acceptLanguageAsLocales(Locale.FRENCH).build();
|
||||
|
||||
MockServerHttpResponse httpResponse = handleRequest(
|
||||
httpRequest, handler, Collections.singletonMap("query", "{greeting}"));
|
||||
@@ -67,7 +105,8 @@ public class GraphQlHttpHandlerTests {
|
||||
.queryFetcher("showId", (env) -> env.getExecutionId().toString())
|
||||
.toHttpHandlerWebFlux();
|
||||
|
||||
MockServerHttpRequest httpRequest = MockServerHttpRequest.post("/").build();
|
||||
MockServerHttpRequest httpRequest = MockServerHttpRequest.post("/")
|
||||
.contentType(MediaType.APPLICATION_GRAPHQL).accept(MediaType.APPLICATION_GRAPHQL).build();
|
||||
|
||||
MockServerHttpResponse httpResponse = handleRequest(
|
||||
httpRequest, handler, Collections.singletonMap("query", "{showId}"));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* 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.
|
||||
@@ -30,6 +30,7 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.context.i18n.LocaleContextHolder;
|
||||
import org.springframework.graphql.GraphQlSetup;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
@@ -44,19 +45,43 @@ import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
/**
|
||||
* Tests for {@link GraphQlHttpHandler}.
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class GraphQlHttpHandlerTests {
|
||||
|
||||
private static final List<HttpMessageConverter<?>> MESSAGE_READERS =
|
||||
Collections.singletonList(new MappingJackson2HttpMessageConverter());
|
||||
|
||||
private final GraphQlHttpHandler greetingHandler = GraphQlSetup.schemaContent("type Query { greeting: String }")
|
||||
.queryFetcher("greeting", (env) -> "Hello").toHttpHandler();
|
||||
|
||||
@Test
|
||||
void shouldProduceApplicationGraphQlByDefault() throws Exception {
|
||||
MockHttpServletRequest servletRequest = createServletRequest("{\"query\":\"{ greeting }\"}", "*/*");
|
||||
MockHttpServletResponse servletResponse = handleRequest(servletRequest, this.greetingHandler);
|
||||
assertThat(servletResponse.getContentType()).isEqualTo(MediaType.APPLICATION_GRAPHQL_VALUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldProduceApplicationGraphQl() throws Exception {
|
||||
MockHttpServletRequest servletRequest = createServletRequest("{\"query\":\"{ greeting }\"}", MediaType.APPLICATION_GRAPHQL_VALUE);
|
||||
MockHttpServletResponse servletResponse = handleRequest(servletRequest, this.greetingHandler);
|
||||
assertThat(servletResponse.getContentType()).isEqualTo(MediaType.APPLICATION_GRAPHQL_VALUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldProduceApplicationJson() throws Exception {
|
||||
MockHttpServletRequest servletRequest = createServletRequest("{\"query\":\"{ greeting }\"}", "application/json");
|
||||
MockHttpServletResponse servletResponse = handleRequest(servletRequest, this.greetingHandler);
|
||||
assertThat(servletResponse.getContentType()).isEqualTo("application/json");
|
||||
}
|
||||
|
||||
@Test
|
||||
void locale() throws Exception {
|
||||
GraphQlHttpHandler handler = GraphQlSetup.schemaContent("type Query { greeting: String }")
|
||||
.queryFetcher("greeting", (env) -> "Hello in " + env.getLocale())
|
||||
.toHttpHandler();
|
||||
MockHttpServletRequest servletRequest = createServletRequest("{\"query\":\"{ greeting }\"}");
|
||||
MockHttpServletRequest servletRequest = createServletRequest("{\"query\":\"{ greeting }\"}", MediaType.APPLICATION_GRAPHQL_VALUE);
|
||||
LocaleContextHolder.setLocale(Locale.FRENCH);
|
||||
|
||||
try {
|
||||
@@ -76,7 +101,7 @@ public class GraphQlHttpHandlerTests {
|
||||
.queryFetcher("showId", (env) -> env.getExecutionId().toString())
|
||||
.toHttpHandler();
|
||||
|
||||
MockHttpServletRequest servletRequest = createServletRequest("{\"query\":\"{ showId }\"}");
|
||||
MockHttpServletRequest servletRequest = createServletRequest("{\"query\":\"{ showId }\"}", MediaType.APPLICATION_GRAPHQL_VALUE);
|
||||
|
||||
MockHttpServletResponse servletResponse = handleRequest(servletRequest, handler);
|
||||
DocumentContext document = JsonPath.parse(servletResponse.getContentAsString());
|
||||
@@ -84,10 +109,11 @@ public class GraphQlHttpHandlerTests {
|
||||
assertThatNoException().isThrownBy(() -> UUID.fromString(id));
|
||||
}
|
||||
|
||||
private MockHttpServletRequest createServletRequest(String query) {
|
||||
private MockHttpServletRequest createServletRequest(String query, String accept) {
|
||||
MockHttpServletRequest servletRequest = new MockHttpServletRequest("POST", "/");
|
||||
servletRequest.setContentType("application/json");
|
||||
servletRequest.setContentType(MediaType.APPLICATION_GRAPHQL_VALUE);
|
||||
servletRequest.setContent(query.getBytes(StandardCharsets.UTF_8));
|
||||
servletRequest.addHeader("Accept", accept);
|
||||
servletRequest.setAsyncSupported(true);
|
||||
return servletRequest;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user