Skip to content
This repository was archived by the owner on Oct 25, 2021. It is now read-only.

Commit 783f7c9

Browse files
#4 Add support for application/graphql content type and post query params
1 parent a0cd9a2 commit 783f7c9

File tree

6 files changed

+230
-33
lines changed

6 files changed

+230
-33
lines changed

graphql-java-spring-webflux/src/main/java/graphql/spring/web/reactive/components/GraphQLController.java

Lines changed: 82 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
package graphql.spring.web.reactive.components;
22

3-
43
import com.fasterxml.jackson.databind.ObjectMapper;
54
import graphql.ExecutionResult;
65
import graphql.Internal;
76
import graphql.spring.web.reactive.ExecutionResultHandler;
87
import graphql.spring.web.reactive.GraphQLInvocation;
98
import graphql.spring.web.reactive.GraphQLInvocationData;
109
import org.springframework.beans.factory.annotation.Autowired;
10+
import org.springframework.http.HttpHeaders;
11+
import org.springframework.http.HttpStatus;
1112
import org.springframework.http.MediaType;
1213
import org.springframework.web.bind.annotation.RequestBody;
14+
import org.springframework.web.bind.annotation.RequestHeader;
1315
import org.springframework.web.bind.annotation.RequestMapping;
1416
import org.springframework.web.bind.annotation.RequestMethod;
1517
import org.springframework.web.bind.annotation.RequestParam;
1618
import org.springframework.web.bind.annotation.RestController;
19+
import org.springframework.web.server.ResponseStatusException;
1720
import org.springframework.web.server.ServerWebExchange;
1821
import reactor.core.publisher.Mono;
1922

@@ -36,16 +39,55 @@ public class GraphQLController {
3639

3740
@RequestMapping(value = "${graphql.url:graphql}",
3841
method = RequestMethod.POST,
39-
consumes = MediaType.APPLICATION_JSON_VALUE,
4042
produces = MediaType.APPLICATION_JSON_VALUE)
41-
public Object graphqlPOST(@RequestBody GraphQLRequestBody body,
42-
ServerWebExchange serverWebExchange) {
43-
String query = body.getQuery();
44-
if (query == null) {
45-
query = "";
43+
public Object graphqlPOST(
44+
@RequestHeader(value = HttpHeaders.CONTENT_TYPE, required = false) String contentType,
45+
@RequestParam(value = "query", required = false) String query,
46+
@RequestParam(value = "operationName", required = false) String operationName,
47+
@RequestParam(value = "variables", required = false) String variablesJson,
48+
@RequestBody(required = false) String body,
49+
ServerWebExchange serverWebExchange) throws IOException {
50+
51+
if (body == null) {
52+
body = "";
4653
}
47-
Mono<ExecutionResult> executionResult = graphQLInvocation.invoke(new GraphQLInvocationData(query, body.getOperationName(), body.getVariables()), serverWebExchange);
48-
return executionResultHandler.handleExecutionResult(executionResult, serverWebExchange.getResponse());
54+
55+
// https://graphql.org/learn/serving-over-http/#post-request
56+
//
57+
// A standard GraphQL POST request should use the application/json content type,
58+
// and include a JSON-encoded body of the following form:
59+
//
60+
// {
61+
// "query": "...",
62+
// "operationName": "...",
63+
// "variables": { "myVariable": "someValue", ... }
64+
// }
65+
66+
if (MediaType.APPLICATION_JSON_VALUE.equals(contentType)) {
67+
GraphQLRequestBody request = objectMapper.readValue(body, GraphQLRequestBody.class);
68+
if (request.getQuery() == null) {
69+
request.setQuery("");
70+
}
71+
return executeRequest(request.getQuery(), request.getOperationName(), request.getVariables(), serverWebExchange);
72+
}
73+
74+
// In addition to the above, we recommend supporting two additional cases:
75+
//
76+
// * If the "query" query string parameter is present (as in the GET example above),
77+
// it should be parsed and handled in the same way as the HTTP GET case.
78+
79+
if (query != null) {
80+
return executeRequest(query, operationName, convertVariablesJson(variablesJson), serverWebExchange);
81+
}
82+
83+
// * If the "application/graphql" Content-Type header is present,
84+
// treat the HTTP POST body contents as the GraphQL query string.
85+
86+
if ("application/graphql".equals(contentType)) {
87+
return executeRequest(body, null, null, serverWebExchange);
88+
}
89+
90+
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "Could not process GraphQL request");
4991
}
5092

5193
@RequestMapping(value = "${graphql.url:graphql}",
@@ -55,10 +97,28 @@ public Object graphqlGET(
5597
@RequestParam("query") String query,
5698
@RequestParam(value = "operationName", required = false) String operationName,
5799
@RequestParam(value = "variables", required = false) String variablesJson,
58-
ServerWebExchange serverWebExchange
59-
) {
60-
Mono<ExecutionResult> executionResult = graphQLInvocation.invoke(new GraphQLInvocationData(query, operationName, convertVariablesJson(variablesJson)), serverWebExchange);
61-
return executionResultHandler.handleExecutionResult(executionResult, serverWebExchange.getResponse());
100+
ServerWebExchange serverWebExchange) {
101+
102+
// https://graphql.org/learn/serving-over-http/#get-request
103+
//
104+
// When receiving an HTTP GET request, the GraphQL query should be specified in the "query" query string.
105+
// For example, if we wanted to execute the following GraphQL query:
106+
//
107+
// {
108+
// me {
109+
// name
110+
// }
111+
// }
112+
//
113+
// This request could be sent via an HTTP GET like so:
114+
//
115+
// http://myapi/graphql?query={me{name}}
116+
//
117+
// Query variables can be sent as a JSON-encoded string in an additional query parameter called "variables".
118+
// If the query contains several named operations,
119+
// an "operationName" query parameter can be used to control which one should be executed.
120+
121+
return executeRequest(query, operationName, convertVariablesJson(variablesJson), serverWebExchange);
62122
}
63123

64124
private Map<String, Object> convertVariablesJson(String jsonMap) {
@@ -71,5 +131,14 @@ private Map<String, Object> convertVariablesJson(String jsonMap) {
71131

72132
}
73133

134+
private Object executeRequest(
135+
String query,
136+
String operationName,
137+
Map<String, Object> variables,
138+
ServerWebExchange serverWebExchange) {
139+
GraphQLInvocationData invocationData = new GraphQLInvocationData(query, operationName, variables);
140+
Mono<ExecutionResult> executionResult = graphQLInvocation.invoke(invocationData, serverWebExchange);
141+
return executionResultHandler.handleExecutionResult(executionResult, serverWebExchange.getResponse());
142+
}
74143

75144
}

graphql-java-spring-webflux/src/test/java/graphql/spring/web/reactive/components/DifferentUrlGraphQLControllerTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public void testDifferentUrl() throws Exception {
5858
Mockito.when(graphql.executeAsync(captor.capture())).thenReturn(cf);
5959

6060
client.post().uri("/otherUrl")
61+
.contentType(MediaType.APPLICATION_JSON)
6162
.body(Mono.just(request), Map.class)
6263
.accept(MediaType.APPLICATION_JSON_UTF8)
6364
.exchange()
@@ -70,4 +71,4 @@ public void testDifferentUrl() throws Exception {
7071
assertThat(captor.getValue().getQuery(), is(query));
7172
}
7273

73-
}
74+
}

graphql-java-spring-webflux/src/test/java/graphql/spring/web/reactive/components/GraphQLControllerTest.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public void testPostRequest() throws Exception {
6464
Mockito.when(graphql.executeAsync(captor.capture())).thenReturn(cf);
6565

6666
client.post().uri("/graphql")
67+
.contentType(MediaType.APPLICATION_JSON)
6768
.body(Mono.just(request), Map.class)
6869
.accept(MediaType.APPLICATION_JSON_UTF8)
6970
.exchange()
@@ -92,6 +93,7 @@ public void testSimplePostRequest() throws Exception {
9293
Mockito.when(graphql.executeAsync(captor.capture())).thenReturn(cf);
9394

9495
client.post().uri("/graphql")
96+
.contentType(MediaType.APPLICATION_JSON)
9597
.body(Mono.just(request), Map.class)
9698
.accept(MediaType.APPLICATION_JSON_UTF8)
9799
.exchange()
@@ -104,6 +106,30 @@ public void testSimplePostRequest() throws Exception {
104106
assertThat(captor.getValue().getQuery(), is(query));
105107
}
106108

109+
@Test
110+
public void testApplicationGraphqlPostRequest() throws Exception {
111+
String query = "{foo}";
112+
113+
ExecutionResultImpl executionResult = ExecutionResultImpl.newExecutionResult()
114+
.data("bar")
115+
.build();
116+
CompletableFuture cf = CompletableFuture.completedFuture(executionResult);
117+
ArgumentCaptor<ExecutionInput> captor = ArgumentCaptor.forClass(ExecutionInput.class);
118+
Mockito.when(graphql.executeAsync(captor.capture())).thenReturn(cf);
119+
120+
client.post().uri("/graphql")
121+
.contentType(new MediaType("application", "graphql"))
122+
.body(Mono.just(query), String.class)
123+
.accept(MediaType.APPLICATION_JSON_UTF8)
124+
.exchange()
125+
.expectStatus().isOk()
126+
.expectBody()
127+
.jsonPath("data").isEqualTo("bar");
128+
129+
assertThat(captor.getAllValues().size(), is(1));
130+
131+
assertThat(captor.getValue().getQuery(), is(query));
132+
}
107133

108134
@Test
109135
public void testGetRequest() throws Exception {
@@ -165,4 +191,4 @@ public void testSimpleGetRequest() throws Exception {
165191
assertThat(captor.getValue().getQuery(), is(query));
166192
}
167193

168-
}
194+
}

graphql-java-spring-webmvc/src/main/java/graphql/spring/web/servlet/components/GraphQLController.java

Lines changed: 83 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
package graphql.spring.web.servlet.components;
22

3-
43
import com.fasterxml.jackson.databind.ObjectMapper;
54
import graphql.ExecutionResult;
65
import graphql.Internal;
76
import graphql.spring.web.servlet.ExecutionResultHandler;
87
import graphql.spring.web.servlet.GraphQLInvocation;
98
import graphql.spring.web.servlet.GraphQLInvocationData;
109
import org.springframework.beans.factory.annotation.Autowired;
10+
import org.springframework.http.HttpHeaders;
11+
import org.springframework.http.HttpStatus;
1112
import org.springframework.http.MediaType;
1213
import org.springframework.web.bind.annotation.RequestBody;
14+
import org.springframework.web.bind.annotation.RequestHeader;
1315
import org.springframework.web.bind.annotation.RequestMapping;
1416
import org.springframework.web.bind.annotation.RequestMethod;
1517
import org.springframework.web.bind.annotation.RequestParam;
1618
import org.springframework.web.bind.annotation.RestController;
1719
import org.springframework.web.context.request.WebRequest;
20+
import org.springframework.web.server.ResponseStatusException;
1821

1922
import java.io.IOException;
2023
import java.util.Collections;
@@ -36,28 +39,86 @@ public class GraphQLController {
3639

3740
@RequestMapping(value = "${graphql.url:graphql}",
3841
method = RequestMethod.POST,
39-
consumes = MediaType.APPLICATION_JSON_VALUE,
40-
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
41-
public Object graphqlPOST(@RequestBody GraphQLRequestBody body,
42-
WebRequest webRequest) {
43-
String query = body.getQuery();
44-
if (query == null) {
45-
query = "";
42+
produces = MediaType.APPLICATION_JSON_VALUE)
43+
public Object graphqlPOST(
44+
@RequestHeader(value = HttpHeaders.CONTENT_TYPE, required = false) String contentType,
45+
@RequestParam(value = "query", required = false) String query,
46+
@RequestParam(value = "operationName", required = false) String operationName,
47+
@RequestParam(value = "variables", required = false) String variablesJson,
48+
@RequestBody(required = false) String body,
49+
WebRequest webRequest) throws IOException {
50+
51+
if (body == null) {
52+
body = "";
4653
}
47-
CompletableFuture<ExecutionResult> executionResult = graphQLInvocation.invoke(new GraphQLInvocationData(query, body.getOperationName(), body.getVariables()), webRequest);
48-
return executionResultHandler.handleExecutionResult(executionResult);
54+
55+
// https://graphql.org/learn/serving-over-http/#post-request
56+
//
57+
// A standard GraphQL POST request should use the application/json content type,
58+
// and include a JSON-encoded body of the following form:
59+
//
60+
// {
61+
// "query": "...",
62+
// "operationName": "...",
63+
// "variables": { "myVariable": "someValue", ... }
64+
// }
65+
66+
if (MediaType.APPLICATION_JSON_VALUE.equals(contentType)) {
67+
GraphQLRequestBody request = objectMapper.readValue(body, GraphQLRequestBody.class);
68+
if (request.getQuery() == null) {
69+
request.setQuery("");
70+
}
71+
return executeRequest(request.getQuery(), request.getOperationName(), request.getVariables(), webRequest);
72+
}
73+
74+
// In addition to the above, we recommend supporting two additional cases:
75+
//
76+
// * If the "query" query string parameter is present (as in the GET example above),
77+
// it should be parsed and handled in the same way as the HTTP GET case.
78+
79+
if (query != null) {
80+
return executeRequest(query, operationName, convertVariablesJson(variablesJson), webRequest);
81+
}
82+
83+
// * If the "application/graphql" Content-Type header is present,
84+
// treat the HTTP POST body contents as the GraphQL query string.
85+
86+
if ("application/graphql".equals(contentType)) {
87+
return executeRequest(body, null, null, webRequest);
88+
}
89+
90+
throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "Could not process GraphQL request");
4991
}
5092

5193
@RequestMapping(value = "${graphql.url:graphql}",
5294
method = RequestMethod.GET,
53-
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
95+
produces = MediaType.APPLICATION_JSON_VALUE)
5496
public Object graphqlGET(
5597
@RequestParam("query") String query,
5698
@RequestParam(value = "operationName", required = false) String operationName,
5799
@RequestParam(value = "variables", required = false) String variablesJson,
58100
WebRequest webRequest) {
59-
CompletableFuture<ExecutionResult> executionResult = graphQLInvocation.invoke(new GraphQLInvocationData(query, operationName, convertVariablesJson(variablesJson)), webRequest);
60-
return executionResultHandler.handleExecutionResult(executionResult);
101+
102+
// https://graphql.org/learn/serving-over-http/#get-request
103+
//
104+
// When receiving an HTTP GET request, the GraphQL query should be specified in the "query" query string.
105+
// For example, if we wanted to execute the following GraphQL query:
106+
//
107+
// {
108+
// me {
109+
// name
110+
// }
111+
// }
112+
//
113+
// This request could be sent via an HTTP GET like so:
114+
//
115+
// http://myapi/graphql?query={me{name}}
116+
//
117+
// Query variables can be sent as a JSON-encoded string in an additional query parameter called "variables".
118+
// If the query contains several named operations,
119+
// an "operationName" query parameter can be used to control which one should be executed.
120+
121+
return executeRequest(query, operationName, convertVariablesJson(variablesJson), webRequest);
61122
}
62123

63124
private Map<String, Object> convertVariablesJson(String jsonMap) {
@@ -70,5 +131,14 @@ private Map<String, Object> convertVariablesJson(String jsonMap) {
70131

71132
}
72133

134+
private Object executeRequest(
135+
String query,
136+
String operationName,
137+
Map<String, Object> variables,
138+
WebRequest webRequest) {
139+
GraphQLInvocationData invocationData = new GraphQLInvocationData(query, operationName, variables);
140+
CompletableFuture<ExecutionResult> executionResult = graphQLInvocation.invoke(invocationData, webRequest);
141+
return executionResultHandler.handleExecutionResult(executionResult);
142+
}
73143

74144
}

graphql-java-spring-webmvc/src/test/java/graphql/spring/web/servlet/components/DifferentUrlGraphQLControllerTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public void testDifferentUrl() throws Exception {
8080

8181
MvcResult mvcResult = this.mockMvc.perform(post("/otherUrl")
8282
.content(toJson(request))
83-
.contentType(MediaType.APPLICATION_JSON_UTF8))
83+
.contentType(MediaType.APPLICATION_JSON))
8484
.andExpect(status().isOk())
8585
.andExpect(request().asyncStarted())
8686
.andReturn();
@@ -98,4 +98,4 @@ public void testDifferentUrl() throws Exception {
9898
}
9999

100100

101-
}
101+
}

0 commit comments

Comments
 (0)