Support "application/graphql+json" media type in GraphQL HTTP mapping

As seen in spring-projects/spring-graphql#108, the GraphQL HTTP spec now
requires the "application/graphql+json" media type and accepts
"application/json" for backwards compatibility.

This commit updates the `RouterFunction` definition for the GraphQL HTTP
endpoints so that both types are accepted.

Closes gh-30407
This commit is contained in:
Brian Clozel 2022-04-04 16:45:11 +02:00
parent d476d8e37b
commit 1c71567c94
7 changed files with 17 additions and 16 deletions

View File

@ -81,8 +81,8 @@ import static org.springframework.web.reactive.function.server.RequestPredicates
@EnableConfigurationProperties(GraphQlCorsProperties.class) @EnableConfigurationProperties(GraphQlCorsProperties.class)
public class GraphQlWebFluxAutoConfiguration { public class GraphQlWebFluxAutoConfiguration {
private static final RequestPredicate ACCEPT_JSON_CONTENT = accept(MediaType.APPLICATION_JSON) private static final RequestPredicate SUPPORTS_MEDIATYPES = accept(MediaType.APPLICATION_GRAPHQL,
.and(contentType(MediaType.APPLICATION_JSON)); MediaType.APPLICATION_JSON).and(contentType(MediaType.APPLICATION_GRAPHQL, MediaType.APPLICATION_JSON));
private static final Log logger = LogFactory.getLog(GraphQlWebFluxAutoConfiguration.class); private static final Log logger = LogFactory.getLog(GraphQlWebFluxAutoConfiguration.class);
@ -107,7 +107,7 @@ public class GraphQlWebFluxAutoConfiguration {
logger.info(LogMessage.format("GraphQL endpoint HTTP POST %s", path)); logger.info(LogMessage.format("GraphQL endpoint HTTP POST %s", path));
RouterFunctions.Builder builder = RouterFunctions.route(); RouterFunctions.Builder builder = RouterFunctions.route();
builder = builder.GET(path, this::onlyAllowPost); builder = builder.GET(path, this::onlyAllowPost);
builder = builder.POST(path, ACCEPT_JSON_CONTENT, httpHandler::handleRequest); builder = builder.POST(path, SUPPORTS_MEDIATYPES, httpHandler::handleRequest);
if (properties.getGraphiql().isEnabled()) { if (properties.getGraphiql().isEnabled()) {
GraphiQlHandler graphQlHandler = new GraphiQlHandler(path, properties.getWebsocket().getPath()); GraphiQlHandler graphQlHandler = new GraphiQlHandler(path, properties.getWebsocket().getPath());
builder = builder.GET(properties.getGraphiql().getPath(), graphQlHandler::handleRequest); builder = builder.GET(properties.getGraphiql().getPath(), graphQlHandler::handleRequest);

View File

@ -87,7 +87,7 @@ public class GraphQlWebMvcAutoConfiguration {
private static final Log logger = LogFactory.getLog(GraphQlWebMvcAutoConfiguration.class); private static final Log logger = LogFactory.getLog(GraphQlWebMvcAutoConfiguration.class);
private static MediaType[] SUPPORTED_MEDIA_TYPES = new MediaType[] { MediaType.valueOf("application/graphql+json"), private static MediaType[] SUPPORTED_MEDIA_TYPES = new MediaType[] { MediaType.APPLICATION_GRAPHQL,
MediaType.APPLICATION_JSON }; MediaType.APPLICATION_JSON };
@Bean @Bean
@ -113,8 +113,8 @@ public class GraphQlWebMvcAutoConfiguration {
logger.info(LogMessage.format("GraphQL endpoint HTTP POST %s", path)); logger.info(LogMessage.format("GraphQL endpoint HTTP POST %s", path));
RouterFunctions.Builder builder = RouterFunctions.route(); RouterFunctions.Builder builder = RouterFunctions.route();
builder = builder.GET(path, this::onlyAllowPost); builder = builder.GET(path, this::onlyAllowPost);
builder = builder.POST(path, RequestPredicates.contentType(MediaType.APPLICATION_JSON) builder = builder.POST(path, RequestPredicates.contentType(SUPPORTED_MEDIA_TYPES)
.and(RequestPredicates.accept(MediaType.APPLICATION_JSON)), httpHandler::handleRequest); .and(RequestPredicates.accept(SUPPORTED_MEDIA_TYPES)), httpHandler::handleRequest);
if (properties.getGraphiql().isEnabled()) { if (properties.getGraphiql().isEnabled()) {
GraphiQlHandler graphiQLHandler = new GraphiQlHandler(path, properties.getWebsocket().getPath()); GraphiQlHandler graphiQLHandler = new GraphiQlHandler(path, properties.getWebsocket().getPath());
builder = builder.GET(properties.getGraphiql().getPath(), graphiQLHandler::handleRequest); builder = builder.GET(properties.getGraphiql().getPath(), graphiQLHandler::handleRequest);

View File

@ -75,7 +75,8 @@ class GraphQlWebFluxAutoConfigurationTests {
testWithWebClient((client) -> { testWithWebClient((client) -> {
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }"; String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }";
client.post().uri("/graphql").bodyValue("{ \"query\": \"" + query + "\"}").exchange().expectStatus().isOk() client.post().uri("/graphql").bodyValue("{ \"query\": \"" + query + "\"}").exchange().expectStatus().isOk()
.expectBody().jsonPath("data.bookById.name").isEqualTo("GraphQL for beginners"); .expectHeader().contentType("application/graphql+json").expectBody().jsonPath("data.bookById.name")
.isEqualTo("GraphQL for beginners");
}); });
} }
@ -150,8 +151,8 @@ class GraphQlWebFluxAutoConfigurationTests {
this.contextRunner.run((context) -> { this.contextRunner.run((context) -> {
WebTestClient client = WebTestClient.bindToApplicationContext(context).configureClient() WebTestClient client = WebTestClient.bindToApplicationContext(context).configureClient()
.defaultHeaders((headers) -> { .defaultHeaders((headers) -> {
headers.setContentType(MediaType.APPLICATION_JSON); headers.setContentType(MediaType.APPLICATION_GRAPHQL);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON)); headers.setAccept(Collections.singletonList(MediaType.APPLICATION_GRAPHQL));
}).baseUrl(BASE_URL).build(); }).baseUrl(BASE_URL).build();
consumer.accept(client); consumer.accept(client);
}); });

View File

@ -81,7 +81,7 @@ class GraphQlWebMvcAutoConfigurationTests {
String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }"; String query = "{ bookById(id: \\\"book-1\\\"){ id name pageCount author } }";
MvcResult result = mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}")).andReturn(); MvcResult result = mockMvc.perform(post("/graphql").content("{\"query\": \"" + query + "\"}")).andReturn();
mockMvc.perform(asyncDispatch(result)).andExpect(status().isOk()) mockMvc.perform(asyncDispatch(result)).andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_GRAPHQL))
.andExpect(jsonPath("data.bookById.name").value("GraphQL for beginners")); .andExpect(jsonPath("data.bookById.name").value("GraphQL for beginners"));
}); });
} }
@ -155,9 +155,9 @@ class GraphQlWebMvcAutoConfigurationTests {
private void testWith(MockMvcConsumer mockMvcConsumer) { private void testWith(MockMvcConsumer mockMvcConsumer) {
this.contextRunner.run((context) -> { this.contextRunner.run((context) -> {
MediaType mediaType = MediaType.APPLICATION_JSON; MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context).defaultRequest(
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context) post("/graphql").contentType(MediaType.APPLICATION_GRAPHQL).accept(MediaType.APPLICATION_GRAPHQL))
.defaultRequest(post("/graphql").contentType(mediaType).accept(mediaType)).build(); .build();
mockMvcConsumer.accept(mockMvc); mockMvcConsumer.accept(mockMvc);
}); });
} }

View File

@ -79,7 +79,7 @@ class HttpGraphQlTesterContextCustomizerIntegrationTests {
@Override @Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) { public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
response.setStatusCode(HttpStatus.OK); response.setStatusCode(HttpStatus.OK);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON); response.getHeaders().setContentType(MediaType.APPLICATION_GRAPHQL);
return response.writeWith(Mono.just(factory.wrap("{\"data\":{}}".getBytes()))); return response.writeWith(Mono.just(factory.wrap("{\"data\":{}}".getBytes())));
} }

View File

@ -79,7 +79,7 @@ class HttpGraphQlTesterContextCustomizerWithCustomBasePathTests {
@Override @Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) { public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
response.setStatusCode(HttpStatus.OK); response.setStatusCode(HttpStatus.OK);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON); response.getHeaders().setContentType(MediaType.APPLICATION_GRAPHQL);
return response.writeWith(Mono.just(factory.wrap("{\"data\":{}}".getBytes()))); return response.writeWith(Mono.just(factory.wrap("{\"data\":{}}".getBytes())));
} }

View File

@ -70,7 +70,7 @@ class HttpGraphQlTesterContextCustomizerWithCustomContextPathTests {
@RestController @RestController
static class TestController { static class TestController {
@PostMapping(path = "/graphql", produces = MediaType.APPLICATION_JSON_VALUE) @PostMapping(path = "/graphql", produces = MediaType.APPLICATION_GRAPHQL_VALUE)
String graphql() { String graphql() {
return "{}"; return "{}";
} }