Skip to content

Commit e455ebb

Browse files
committed
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
1 parent 2cdaf34 commit e455ebb

File tree

9 files changed

+117
-19
lines changed

9 files changed

+117
-19
lines changed

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ configure(moduleProjects) {
5959
imports {
6060
mavenBom "com.fasterxml.jackson:jackson-bom:2.13.1"
6161
mavenBom "io.projectreactor:reactor-bom:2020.0.17"
62-
mavenBom "org.springframework:spring-framework-bom:5.3.17"
62+
mavenBom "org.springframework:spring-framework-bom:5.3.19-SNAPSHOT"
6363
mavenBom "org.springframework.data:spring-data-bom:2021.2.0-M4"
6464
mavenBom "org.springframework.security:spring-security-bom:5.7.0-M3"
6565
mavenBom "com.querydsl:querydsl-bom:5.0.0"

spring-graphql-docs/src/docs/asciidoc/index.adoc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ request body, as defined in the proposed
5656
https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md[GraphQL over HTTP]
5757
specification. Once the JSON body has been successfully decoded, the HTTP response
5858
status is always 200 (OK), and any errors from GraphQL request execution appear in the
59-
"errors" section of the GraphQL response.
59+
"errors" section of the GraphQL response. The default and preferred choice of media type is
60+
`"application/graphql+json"`, but `"application/json"` is also supported, as described in the
61+
specification.
6062

6163
`GraphQlHttpHandler` can be exposed as an HTTP endpoint by declaring a `RouterFunction`
6264
bean and using the `RouterFunctions` from Spring MVC or WebFlux to create the route. The

spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebTestClientTransport.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,12 @@ final class WebTestClientTransport implements GraphQlTransport {
5555
public Mono<GraphQlResponse> execute(GraphQlRequest request) {
5656

5757
Map<String, Object> responseMap = this.webTestClient.post()
58-
.contentType(MediaType.APPLICATION_JSON)
59-
.accept(MediaType.APPLICATION_JSON)
58+
.contentType(MediaType.APPLICATION_GRAPHQL)
59+
.accept(MediaType.APPLICATION_GRAPHQL)
6060
.bodyValue(request.toMap())
6161
.exchange()
6262
.expectStatus().isOk()
63-
.expectHeader().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)
63+
.expectHeader().contentTypeCompatibleWith(MediaType.APPLICATION_GRAPHQL)
6464
.expectBody(MAP_TYPE)
6565
.returnResult()
6666
.getResponseBody();

spring-graphql/src/main/java/org/springframework/graphql/client/DefaultRSocketGraphQlClientBuilder.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.messaging.rsocket.RSocketStrategies;
2929
import org.springframework.util.Assert;
3030
import org.springframework.util.MimeType;
31+
import org.springframework.util.MimeTypeUtils;
3132

3233

3334
/**
@@ -60,8 +61,7 @@ final class DefaultRSocketGraphQlClientBuilder
6061
}
6162

6263
private static RSocketRequester.Builder initRSocketRequestBuilder() {
63-
MimeType mimeType = MimeType.valueOf("application/graphql+json");
64-
RSocketRequester.Builder requesterBuilder = RSocketRequester.builder().dataMimeType(mimeType);
64+
RSocketRequester.Builder requesterBuilder = RSocketRequester.builder().dataMimeType(MimeTypeUtils.APPLICATION_GRAPHQL);
6565
if (jackson2Present) {
6666
requesterBuilder.rsocketStrategies(
6767
RSocketStrategies.builder()

spring-graphql/src/main/java/org/springframework/graphql/client/HttpGraphQlTransport.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ final class HttpGraphQlTransport implements GraphQlTransport {
5656
@Override
5757
public Mono<GraphQlResponse> execute(GraphQlRequest request) {
5858
return this.webClient.post()
59-
.contentType(MediaType.APPLICATION_JSON)
60-
.accept(MediaType.APPLICATION_JSON)
59+
.contentType(MediaType.APPLICATION_GRAPHQL)
60+
.accept(MediaType.APPLICATION_GRAPHQL, MediaType.APPLICATION_JSON)
6161
.bodyValue(request.toMap())
6262
.retrieve()
6363
.bodyToMono(MAP_TYPE)

spring-graphql/src/main/java/org/springframework/graphql/server/webflux/GraphQlHttpHandler.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.graphql.server.webflux;
1818

19+
import java.util.Arrays;
20+
import java.util.List;
1921
import java.util.Map;
2022

2123
import org.apache.commons.logging.Log;
@@ -25,6 +27,7 @@
2527
import org.springframework.core.ParameterizedTypeReference;
2628
import org.springframework.graphql.server.WebGraphQlHandler;
2729
import org.springframework.graphql.server.WebGraphQlRequest;
30+
import org.springframework.http.MediaType;
2831
import org.springframework.util.Assert;
2932
import org.springframework.web.reactive.function.server.ServerRequest;
3033
import org.springframework.web.reactive.function.server.ServerResponse;
@@ -44,6 +47,8 @@ public class GraphQlHttpHandler {
4447
new ParameterizedTypeReference<Map<String, Object>>() {
4548
};
4649

50+
private static final List<MediaType> SUPPORTED_MEDIA_TYPES = Arrays.asList(MediaType.APPLICATION_GRAPHQL, MediaType.APPLICATION_JSON);
51+
4752
private final WebGraphQlHandler graphQlHandler;
4853

4954
/**
@@ -78,8 +83,18 @@ public Mono<ServerResponse> handleRequest(ServerRequest serverRequest) {
7883
}
7984
ServerResponse.BodyBuilder builder = ServerResponse.ok();
8085
builder.headers(headers -> headers.putAll(response.getResponseHeaders()));
86+
builder.contentType(selectResponseMediaType(serverRequest));
8187
return builder.bodyValue(response.toMap());
8288
});
8389
}
8490

91+
private static MediaType selectResponseMediaType(ServerRequest serverRequest) {
92+
for (MediaType accepted : serverRequest.headers().accept()) {
93+
if (SUPPORTED_MEDIA_TYPES.contains(accepted)) {
94+
return accepted;
95+
}
96+
}
97+
return MediaType.APPLICATION_GRAPHQL;
98+
}
99+
85100
}

spring-graphql/src/main/java/org/springframework/graphql/server/webmvc/GraphQlHttpHandler.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package org.springframework.graphql.server.webmvc;
1818

1919
import java.io.IOException;
20+
import java.util.Arrays;
21+
import java.util.List;
2022
import java.util.Map;
2123

2224
import javax.servlet.ServletException;
@@ -29,6 +31,7 @@
2931
import org.springframework.core.ParameterizedTypeReference;
3032
import org.springframework.graphql.server.WebGraphQlHandler;
3133
import org.springframework.graphql.server.WebGraphQlRequest;
34+
import org.springframework.http.MediaType;
3235
import org.springframework.util.AlternativeJdkIdGenerator;
3336
import org.springframework.util.Assert;
3437
import org.springframework.util.IdGenerator;
@@ -50,7 +53,10 @@ public class GraphQlHttpHandler {
5053
private static final Log logger = LogFactory.getLog(GraphQlHttpHandler.class);
5154

5255
private static final ParameterizedTypeReference<Map<String, Object>> MAP_PARAMETERIZED_TYPE_REF =
53-
new ParameterizedTypeReference<Map<String, Object>>() {};
56+
new ParameterizedTypeReference<Map<String, Object>>() {
57+
};
58+
59+
private static final List<MediaType> SUPPORTED_MEDIA_TYPES = Arrays.asList(MediaType.APPLICATION_GRAPHQL, MediaType.APPLICATION_JSON);
5460

5561
private final IdGenerator idGenerator = new AlternativeJdkIdGenerator();
5662

@@ -89,6 +95,7 @@ public ServerResponse handleRequest(ServerRequest serverRequest) throws ServletE
8995
}
9096
ServerResponse.BodyBuilder builder = ServerResponse.ok();
9197
builder.headers(headers -> headers.putAll(response.getResponseHeaders()));
98+
builder.contentType(selectResponseMediaType(serverRequest));
9299
return builder.body(response.toMap());
93100
});
94101

@@ -104,4 +111,13 @@ private static Map<String, Object> readBody(ServerRequest request) throws Servle
104111
}
105112
}
106113

114+
private static MediaType selectResponseMediaType(ServerRequest serverRequest) {
115+
for (MediaType accepted : serverRequest.headers().accept()) {
116+
if (SUPPORTED_MEDIA_TYPES.contains(accepted)) {
117+
return accepted;
118+
}
119+
}
120+
return MediaType.APPLICATION_GRAPHQL;
121+
}
122+
107123
}

spring-graphql/src/test/java/org/springframework/graphql/server/webflux/GraphQlHttpHandlerTests.java

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,6 +26,7 @@
2626
import reactor.core.publisher.Mono;
2727

2828
import org.springframework.graphql.GraphQlSetup;
29+
import org.springframework.http.MediaType;
2930
import org.springframework.http.codec.EncoderHttpMessageWriter;
3031
import org.springframework.http.codec.HttpMessageWriter;
3132
import org.springframework.http.codec.json.Jackson2JsonEncoder;
@@ -45,14 +46,51 @@
4546
*/
4647
public class GraphQlHttpHandlerTests {
4748

49+
private final GraphQlHttpHandler greetingHandler = GraphQlSetup.schemaContent("type Query { greeting: String }")
50+
.queryFetcher("greeting", (env) -> "Hello").toHttpHandlerWebFlux();
51+
52+
53+
@Test
54+
void shouldProduceApplicationGraphQlByDefault() {
55+
MockServerHttpRequest httpRequest = MockServerHttpRequest.post("/")
56+
.contentType(MediaType.APPLICATION_GRAPHQL).accept(MediaType.ALL).build();
57+
58+
MockServerHttpResponse httpResponse = handleRequest(
59+
httpRequest, this.greetingHandler, Collections.singletonMap("query", "{greeting}"));
60+
61+
assertThat(httpResponse.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_GRAPHQL);
62+
}
63+
64+
@Test
65+
void shouldProduceApplicationGraphQl() {
66+
MockServerHttpRequest httpRequest = MockServerHttpRequest.post("/")
67+
.contentType(MediaType.APPLICATION_GRAPHQL).accept(MediaType.APPLICATION_GRAPHQL).build();
68+
69+
MockServerHttpResponse httpResponse = handleRequest(
70+
httpRequest, this.greetingHandler, Collections.singletonMap("query", "{greeting}"));
71+
72+
assertThat(httpResponse.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_GRAPHQL);
73+
}
74+
75+
@Test
76+
void shouldProduceApplicationJson() {
77+
MockServerHttpRequest httpRequest = MockServerHttpRequest.post("/")
78+
.contentType(MediaType.APPLICATION_GRAPHQL).accept(MediaType.APPLICATION_JSON).build();
79+
80+
MockServerHttpResponse httpResponse = handleRequest(
81+
httpRequest, this.greetingHandler, Collections.singletonMap("query", "{greeting}"));
82+
83+
assertThat(httpResponse.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
84+
}
85+
4886
@Test
4987
void locale() {
5088
GraphQlHttpHandler handler = GraphQlSetup.schemaContent("type Query { greeting: String }")
5189
.queryFetcher("greeting", (env) -> "Hello in " + env.getLocale())
5290
.toHttpHandlerWebFlux();
5391

54-
MockServerHttpRequest httpRequest =
55-
MockServerHttpRequest.post("/").acceptLanguageAsLocales(Locale.FRENCH).build();
92+
MockServerHttpRequest httpRequest = MockServerHttpRequest.post("/")
93+
.contentType(MediaType.APPLICATION_GRAPHQL).accept(MediaType.APPLICATION_GRAPHQL).acceptLanguageAsLocales(Locale.FRENCH).build();
5694

5795
MockServerHttpResponse httpResponse = handleRequest(
5896
httpRequest, handler, Collections.singletonMap("query", "{greeting}"));
@@ -67,7 +105,8 @@ void shouldSetExecutionId() {
67105
.queryFetcher("showId", (env) -> env.getExecutionId().toString())
68106
.toHttpHandlerWebFlux();
69107

70-
MockServerHttpRequest httpRequest = MockServerHttpRequest.post("/").build();
108+
MockServerHttpRequest httpRequest = MockServerHttpRequest.post("/")
109+
.contentType(MediaType.APPLICATION_GRAPHQL).accept(MediaType.APPLICATION_GRAPHQL).build();
71110

72111
MockServerHttpResponse httpResponse = handleRequest(
73112
httpRequest, handler, Collections.singletonMap("query", "{showId}"));

spring-graphql/src/test/java/org/springframework/graphql/server/webmvc/GraphQlHttpHandlerTests.java

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@
3030

3131
import org.springframework.context.i18n.LocaleContextHolder;
3232
import org.springframework.graphql.GraphQlSetup;
33+
import org.springframework.http.MediaType;
3334
import org.springframework.http.converter.HttpMessageConverter;
3435
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
3536
import org.springframework.mock.web.MockHttpServletRequest;
@@ -44,19 +45,43 @@
4445
/**
4546
* Tests for {@link GraphQlHttpHandler}.
4647
* @author Rossen Stoyanchev
48+
* @author Brian Clozel
4749
*/
4850
public class GraphQlHttpHandlerTests {
4951

5052
private static final List<HttpMessageConverter<?>> MESSAGE_READERS =
5153
Collections.singletonList(new MappingJackson2HttpMessageConverter());
5254

55+
private final GraphQlHttpHandler greetingHandler = GraphQlSetup.schemaContent("type Query { greeting: String }")
56+
.queryFetcher("greeting", (env) -> "Hello").toHttpHandler();
57+
58+
@Test
59+
void shouldProduceApplicationGraphQlByDefault() throws Exception {
60+
MockHttpServletRequest servletRequest = createServletRequest("{\"query\":\"{ greeting }\"}", "*/*");
61+
MockHttpServletResponse servletResponse = handleRequest(servletRequest, this.greetingHandler);
62+
assertThat(servletResponse.getContentType()).isEqualTo(MediaType.APPLICATION_GRAPHQL_VALUE);
63+
}
64+
65+
@Test
66+
void shouldProduceApplicationGraphQl() throws Exception {
67+
MockHttpServletRequest servletRequest = createServletRequest("{\"query\":\"{ greeting }\"}", MediaType.APPLICATION_GRAPHQL_VALUE);
68+
MockHttpServletResponse servletResponse = handleRequest(servletRequest, this.greetingHandler);
69+
assertThat(servletResponse.getContentType()).isEqualTo(MediaType.APPLICATION_GRAPHQL_VALUE);
70+
}
71+
72+
@Test
73+
void shouldProduceApplicationJson() throws Exception {
74+
MockHttpServletRequest servletRequest = createServletRequest("{\"query\":\"{ greeting }\"}", "application/json");
75+
MockHttpServletResponse servletResponse = handleRequest(servletRequest, this.greetingHandler);
76+
assertThat(servletResponse.getContentType()).isEqualTo("application/json");
77+
}
5378

5479
@Test
5580
void locale() throws Exception {
5681
GraphQlHttpHandler handler = GraphQlSetup.schemaContent("type Query { greeting: String }")
5782
.queryFetcher("greeting", (env) -> "Hello in " + env.getLocale())
5883
.toHttpHandler();
59-
MockHttpServletRequest servletRequest = createServletRequest("{\"query\":\"{ greeting }\"}");
84+
MockHttpServletRequest servletRequest = createServletRequest("{\"query\":\"{ greeting }\"}", MediaType.APPLICATION_GRAPHQL_VALUE);
6085
LocaleContextHolder.setLocale(Locale.FRENCH);
6186

6287
try {
@@ -76,18 +101,19 @@ void shouldSetExecutionId() throws Exception {
76101
.queryFetcher("showId", (env) -> env.getExecutionId().toString())
77102
.toHttpHandler();
78103

79-
MockHttpServletRequest servletRequest = createServletRequest("{\"query\":\"{ showId }\"}");
104+
MockHttpServletRequest servletRequest = createServletRequest("{\"query\":\"{ showId }\"}", MediaType.APPLICATION_GRAPHQL_VALUE);
80105

81106
MockHttpServletResponse servletResponse = handleRequest(servletRequest, handler);
82107
DocumentContext document = JsonPath.parse(servletResponse.getContentAsString());
83108
String id = document.read("data.showId", String.class);
84109
assertThatNoException().isThrownBy(() -> UUID.fromString(id));
85110
}
86111

87-
private MockHttpServletRequest createServletRequest(String query) {
112+
private MockHttpServletRequest createServletRequest(String query, String accept) {
88113
MockHttpServletRequest servletRequest = new MockHttpServletRequest("POST", "/");
89-
servletRequest.setContentType("application/json");
114+
servletRequest.setContentType(MediaType.APPLICATION_GRAPHQL_VALUE);
90115
servletRequest.setContent(query.getBytes(StandardCharsets.UTF_8));
116+
servletRequest.addHeader("Accept", accept);
91117
servletRequest.setAsyncSupported(true);
92118
return servletRequest;
93119
}

0 commit comments

Comments
 (0)