diff --git a/graphql-java-spring-webflux/src/main/java/graphql/spring/web/reactive/components/GraphQLController.java b/graphql-java-spring-webflux/src/main/java/graphql/spring/web/reactive/components/GraphQLController.java index c367dc1..7281799 100644 --- a/graphql-java-spring-webflux/src/main/java/graphql/spring/web/reactive/components/GraphQLController.java +++ b/graphql-java-spring-webflux/src/main/java/graphql/spring/web/reactive/components/GraphQLController.java @@ -1,6 +1,5 @@ package graphql.spring.web.reactive.components; - import com.fasterxml.jackson.databind.ObjectMapper; import graphql.ExecutionResult; import graphql.Internal; @@ -8,12 +7,16 @@ import graphql.spring.web.reactive.GraphQLInvocation; import graphql.spring.web.reactive.GraphQLInvocationData; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @@ -36,16 +39,55 @@ public class GraphQLController { @RequestMapping(value = "${graphql.url:graphql}", method = RequestMethod.POST, - consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public Object graphqlPOST(@RequestBody GraphQLRequestBody body, - ServerWebExchange serverWebExchange) { - String query = body.getQuery(); - if (query == null) { - query = ""; + public Object graphqlPOST( + @RequestHeader(value = HttpHeaders.CONTENT_TYPE, required = false) String contentType, + @RequestParam(value = "query", required = false) String query, + @RequestParam(value = "operationName", required = false) String operationName, + @RequestParam(value = "variables", required = false) String variablesJson, + @RequestBody(required = false) String body, + ServerWebExchange serverWebExchange) throws IOException { + + if (body == null) { + body = ""; } - Mono executionResult = graphQLInvocation.invoke(new GraphQLInvocationData(query, body.getOperationName(), body.getVariables()), serverWebExchange); - return executionResultHandler.handleExecutionResult(executionResult, serverWebExchange.getResponse()); + + // https://graphql.org/learn/serving-over-http/#post-request + // + // A standard GraphQL POST request should use the application/json content type, + // and include a JSON-encoded body of the following form: + // + // { + // "query": "...", + // "operationName": "...", + // "variables": { "myVariable": "someValue", ... } + // } + + if (MediaType.APPLICATION_JSON_VALUE.equals(contentType)) { + GraphQLRequestBody request = objectMapper.readValue(body, GraphQLRequestBody.class); + if (request.getQuery() == null) { + request.setQuery(""); + } + return executeRequest(request.getQuery(), request.getOperationName(), request.getVariables(), serverWebExchange); + } + + // In addition to the above, we recommend supporting two additional cases: + // + // * If the "query" query string parameter is present (as in the GET example above), + // it should be parsed and handled in the same way as the HTTP GET case. + + if (query != null) { + return executeRequest(query, operationName, convertVariablesJson(variablesJson), serverWebExchange); + } + + // * If the "application/graphql" Content-Type header is present, + // treat the HTTP POST body contents as the GraphQL query string. + + if ("application/graphql".equals(contentType)) { + return executeRequest(body, null, null, serverWebExchange); + } + + throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "Could not process GraphQL request"); } @RequestMapping(value = "${graphql.url:graphql}", @@ -55,10 +97,28 @@ public Object graphqlGET( @RequestParam("query") String query, @RequestParam(value = "operationName", required = false) String operationName, @RequestParam(value = "variables", required = false) String variablesJson, - ServerWebExchange serverWebExchange - ) { - Mono executionResult = graphQLInvocation.invoke(new GraphQLInvocationData(query, operationName, convertVariablesJson(variablesJson)), serverWebExchange); - return executionResultHandler.handleExecutionResult(executionResult, serverWebExchange.getResponse()); + ServerWebExchange serverWebExchange) { + + // https://graphql.org/learn/serving-over-http/#get-request + // + // When receiving an HTTP GET request, the GraphQL query should be specified in the "query" query string. + // For example, if we wanted to execute the following GraphQL query: + // + // { + // me { + // name + // } + // } + // + // This request could be sent via an HTTP GET like so: + // + // http://myapi/graphql?query={me{name}} + // + // Query variables can be sent as a JSON-encoded string in an additional query parameter called "variables". + // If the query contains several named operations, + // an "operationName" query parameter can be used to control which one should be executed. + + return executeRequest(query, operationName, convertVariablesJson(variablesJson), serverWebExchange); } private Map convertVariablesJson(String jsonMap) { @@ -71,5 +131,14 @@ private Map convertVariablesJson(String jsonMap) { } + private Object executeRequest( + String query, + String operationName, + Map variables, + ServerWebExchange serverWebExchange) { + GraphQLInvocationData invocationData = new GraphQLInvocationData(query, operationName, variables); + Mono executionResult = graphQLInvocation.invoke(invocationData, serverWebExchange); + return executionResultHandler.handleExecutionResult(executionResult, serverWebExchange.getResponse()); + } } diff --git a/graphql-java-spring-webflux/src/test/java/graphql/spring/web/reactive/components/DifferentUrlGraphQLControllerTest.java b/graphql-java-spring-webflux/src/test/java/graphql/spring/web/reactive/components/DifferentUrlGraphQLControllerTest.java index dd69419..8a7d32a 100644 --- a/graphql-java-spring-webflux/src/test/java/graphql/spring/web/reactive/components/DifferentUrlGraphQLControllerTest.java +++ b/graphql-java-spring-webflux/src/test/java/graphql/spring/web/reactive/components/DifferentUrlGraphQLControllerTest.java @@ -58,6 +58,7 @@ public void testDifferentUrl() throws Exception { Mockito.when(graphql.executeAsync(captor.capture())).thenReturn(cf); client.post().uri("/otherUrl") + .contentType(MediaType.APPLICATION_JSON) .body(Mono.just(request), Map.class) .accept(MediaType.APPLICATION_JSON_UTF8) .exchange() @@ -70,4 +71,4 @@ public void testDifferentUrl() throws Exception { assertThat(captor.getValue().getQuery(), is(query)); } -} \ No newline at end of file +} diff --git a/graphql-java-spring-webflux/src/test/java/graphql/spring/web/reactive/components/GraphQLControllerTest.java b/graphql-java-spring-webflux/src/test/java/graphql/spring/web/reactive/components/GraphQLControllerTest.java index fd2f680..ea57695 100644 --- a/graphql-java-spring-webflux/src/test/java/graphql/spring/web/reactive/components/GraphQLControllerTest.java +++ b/graphql-java-spring-webflux/src/test/java/graphql/spring/web/reactive/components/GraphQLControllerTest.java @@ -64,6 +64,7 @@ public void testPostRequest() throws Exception { Mockito.when(graphql.executeAsync(captor.capture())).thenReturn(cf); client.post().uri("/graphql") + .contentType(MediaType.APPLICATION_JSON) .body(Mono.just(request), Map.class) .accept(MediaType.APPLICATION_JSON_UTF8) .exchange() @@ -92,6 +93,7 @@ public void testSimplePostRequest() throws Exception { Mockito.when(graphql.executeAsync(captor.capture())).thenReturn(cf); client.post().uri("/graphql") + .contentType(MediaType.APPLICATION_JSON) .body(Mono.just(request), Map.class) .accept(MediaType.APPLICATION_JSON_UTF8) .exchange() @@ -104,6 +106,92 @@ public void testSimplePostRequest() throws Exception { assertThat(captor.getValue().getQuery(), is(query)); } + @Test + public void testQueryParamPostRequest() throws Exception { + String variablesJson = "{\"variable\":\"variableValue\"}"; + String variablesValue = URLEncoder.encode(variablesJson, "UTF-8"); + String query = "query myQuery {foo}"; + String queryString = URLEncoder.encode(query, "UTF-8"); + String operationName = "myQuery"; + + ExecutionResultImpl executionResult = ExecutionResultImpl.newExecutionResult() + .data("bar") + .build(); + CompletableFuture cf = CompletableFuture.completedFuture(executionResult); + ArgumentCaptor captor = ArgumentCaptor.forClass(ExecutionInput.class); + Mockito.when(graphql.executeAsync(captor.capture())).thenReturn(cf); + + client.post().uri(uriBuilder -> uriBuilder.path("/graphql") + .queryParam("variables", variablesValue) + .queryParam("query", queryString) + .queryParam("operationName", operationName) + .build(variablesJson, queryString)) + .accept(MediaType.APPLICATION_JSON_UTF8) + .exchange() + .expectStatus().isOk() + .expectBody() + .jsonPath("data").isEqualTo("bar"); + + assertThat(captor.getAllValues().size(), is(1)); + + Map variables = new LinkedHashMap<>(); + variables.put("variable", "variableValue"); + assertThat(captor.getValue().getQuery(), is(query)); + assertThat(captor.getValue().getVariables(), is(variables)); + assertThat(captor.getValue().getOperationName(), is(operationName)); + } + + @Test + public void testSimpleQueryParamPostRequest() throws Exception { + String query = "{foo}"; + String queryString = URLEncoder.encode(query, "UTF-8"); + + ExecutionResultImpl executionResult = ExecutionResultImpl.newExecutionResult() + .data("bar") + .build(); + CompletableFuture cf = CompletableFuture.completedFuture(executionResult); + ArgumentCaptor captor = ArgumentCaptor.forClass(ExecutionInput.class); + Mockito.when(graphql.executeAsync(captor.capture())).thenReturn(cf); + + client.post().uri(uriBuilder -> uriBuilder.path("/graphql") + .queryParam("query", queryString) + .build()) + .accept(MediaType.APPLICATION_JSON_UTF8) + .exchange() + .expectStatus().isOk() + .expectBody() + .jsonPath("data").isEqualTo("bar"); + + + assertThat(captor.getAllValues().size(), is(1)); + + assertThat(captor.getValue().getQuery(), is(query)); + } + + @Test + public void testApplicationGraphqlPostRequest() throws Exception { + String query = "{foo}"; + + ExecutionResultImpl executionResult = ExecutionResultImpl.newExecutionResult() + .data("bar") + .build(); + CompletableFuture cf = CompletableFuture.completedFuture(executionResult); + ArgumentCaptor captor = ArgumentCaptor.forClass(ExecutionInput.class); + Mockito.when(graphql.executeAsync(captor.capture())).thenReturn(cf); + + client.post().uri("/graphql") + .contentType(new MediaType("application", "graphql")) + .body(Mono.just(query), String.class) + .accept(MediaType.APPLICATION_JSON_UTF8) + .exchange() + .expectStatus().isOk() + .expectBody() + .jsonPath("data").isEqualTo("bar"); + + assertThat(captor.getAllValues().size(), is(1)); + + assertThat(captor.getValue().getQuery(), is(query)); + } @Test public void testGetRequest() throws Exception { @@ -165,4 +253,4 @@ public void testSimpleGetRequest() throws Exception { assertThat(captor.getValue().getQuery(), is(query)); } -} \ No newline at end of file +} diff --git a/graphql-java-spring-webmvc/src/main/java/graphql/spring/web/servlet/components/GraphQLController.java b/graphql-java-spring-webmvc/src/main/java/graphql/spring/web/servlet/components/GraphQLController.java index dc2c7e0..700e8c5 100644 --- a/graphql-java-spring-webmvc/src/main/java/graphql/spring/web/servlet/components/GraphQLController.java +++ b/graphql-java-spring-webmvc/src/main/java/graphql/spring/web/servlet/components/GraphQLController.java @@ -1,6 +1,5 @@ package graphql.spring.web.servlet.components; - import com.fasterxml.jackson.databind.ObjectMapper; import graphql.ExecutionResult; import graphql.Internal; @@ -8,13 +7,17 @@ import graphql.spring.web.servlet.GraphQLInvocation; import graphql.spring.web.servlet.GraphQLInvocationData; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.WebRequest; +import org.springframework.web.server.ResponseStatusException; import java.io.IOException; import java.util.Collections; @@ -36,28 +39,86 @@ public class GraphQLController { @RequestMapping(value = "${graphql.url:graphql}", method = RequestMethod.POST, - consumes = MediaType.APPLICATION_JSON_VALUE, - produces = MediaType.APPLICATION_JSON_UTF8_VALUE) - public Object graphqlPOST(@RequestBody GraphQLRequestBody body, - WebRequest webRequest) { - String query = body.getQuery(); - if (query == null) { - query = ""; + produces = MediaType.APPLICATION_JSON_VALUE) + public Object graphqlPOST( + @RequestHeader(value = HttpHeaders.CONTENT_TYPE, required = false) String contentType, + @RequestParam(value = "query", required = false) String query, + @RequestParam(value = "operationName", required = false) String operationName, + @RequestParam(value = "variables", required = false) String variablesJson, + @RequestBody(required = false) String body, + WebRequest webRequest) throws IOException { + + if (body == null) { + body = ""; } - CompletableFuture executionResult = graphQLInvocation.invoke(new GraphQLInvocationData(query, body.getOperationName(), body.getVariables()), webRequest); - return executionResultHandler.handleExecutionResult(executionResult); + + // https://graphql.org/learn/serving-over-http/#post-request + // + // A standard GraphQL POST request should use the application/json content type, + // and include a JSON-encoded body of the following form: + // + // { + // "query": "...", + // "operationName": "...", + // "variables": { "myVariable": "someValue", ... } + // } + + if (MediaType.APPLICATION_JSON_VALUE.equals(contentType)) { + GraphQLRequestBody request = objectMapper.readValue(body, GraphQLRequestBody.class); + if (request.getQuery() == null) { + request.setQuery(""); + } + return executeRequest(request.getQuery(), request.getOperationName(), request.getVariables(), webRequest); + } + + // In addition to the above, we recommend supporting two additional cases: + // + // * If the "query" query string parameter is present (as in the GET example above), + // it should be parsed and handled in the same way as the HTTP GET case. + + if (query != null) { + return executeRequest(query, operationName, convertVariablesJson(variablesJson), webRequest); + } + + // * If the "application/graphql" Content-Type header is present, + // treat the HTTP POST body contents as the GraphQL query string. + + if ("application/graphql".equals(contentType)) { + return executeRequest(body, null, null, webRequest); + } + + throw new ResponseStatusException(HttpStatus.UNPROCESSABLE_ENTITY, "Could not process GraphQL request"); } @RequestMapping(value = "${graphql.url:graphql}", method = RequestMethod.GET, - produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + produces = MediaType.APPLICATION_JSON_VALUE) public Object graphqlGET( @RequestParam("query") String query, @RequestParam(value = "operationName", required = false) String operationName, @RequestParam(value = "variables", required = false) String variablesJson, WebRequest webRequest) { - CompletableFuture executionResult = graphQLInvocation.invoke(new GraphQLInvocationData(query, operationName, convertVariablesJson(variablesJson)), webRequest); - return executionResultHandler.handleExecutionResult(executionResult); + + // https://graphql.org/learn/serving-over-http/#get-request + // + // When receiving an HTTP GET request, the GraphQL query should be specified in the "query" query string. + // For example, if we wanted to execute the following GraphQL query: + // + // { + // me { + // name + // } + // } + // + // This request could be sent via an HTTP GET like so: + // + // http://myapi/graphql?query={me{name}} + // + // Query variables can be sent as a JSON-encoded string in an additional query parameter called "variables". + // If the query contains several named operations, + // an "operationName" query parameter can be used to control which one should be executed. + + return executeRequest(query, operationName, convertVariablesJson(variablesJson), webRequest); } private Map convertVariablesJson(String jsonMap) { @@ -70,5 +131,14 @@ private Map convertVariablesJson(String jsonMap) { } + private Object executeRequest( + String query, + String operationName, + Map variables, + WebRequest webRequest) { + GraphQLInvocationData invocationData = new GraphQLInvocationData(query, operationName, variables); + CompletableFuture executionResult = graphQLInvocation.invoke(invocationData, webRequest); + return executionResultHandler.handleExecutionResult(executionResult); + } } diff --git a/graphql-java-spring-webmvc/src/test/java/graphql/spring/web/servlet/components/DifferentUrlGraphQLControllerTest.java b/graphql-java-spring-webmvc/src/test/java/graphql/spring/web/servlet/components/DifferentUrlGraphQLControllerTest.java index a8f2c6c..f263d7c 100644 --- a/graphql-java-spring-webmvc/src/test/java/graphql/spring/web/servlet/components/DifferentUrlGraphQLControllerTest.java +++ b/graphql-java-spring-webmvc/src/test/java/graphql/spring/web/servlet/components/DifferentUrlGraphQLControllerTest.java @@ -80,7 +80,7 @@ public void testDifferentUrl() throws Exception { MvcResult mvcResult = this.mockMvc.perform(post("/otherUrl") .content(toJson(request)) - .contentType(MediaType.APPLICATION_JSON_UTF8)) + .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(request().asyncStarted()) .andReturn(); @@ -98,4 +98,4 @@ public void testDifferentUrl() throws Exception { } -} \ No newline at end of file +} diff --git a/graphql-java-spring-webmvc/src/test/java/graphql/spring/web/servlet/components/GraphQLControllerTest.java b/graphql-java-spring-webmvc/src/test/java/graphql/spring/web/servlet/components/GraphQLControllerTest.java index bece066..3594d8c 100644 --- a/graphql-java-spring-webmvc/src/test/java/graphql/spring/web/servlet/components/GraphQLControllerTest.java +++ b/graphql-java-spring-webmvc/src/test/java/graphql/spring/web/servlet/components/GraphQLControllerTest.java @@ -86,7 +86,7 @@ public void testPostRequest() throws Exception { MvcResult mvcResult = this.mockMvc.perform(post("/graphql") .content(toJson(request)) - .contentType(MediaType.APPLICATION_JSON_UTF8)) + .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(request().asyncStarted()) .andReturn(); @@ -121,7 +121,108 @@ public void testSimplePostRequest() throws Exception { MvcResult mvcResult = this.mockMvc.perform(post("/graphql") .content(toJson(request)) - .contentType(MediaType.APPLICATION_JSON_UTF8)) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(request().asyncStarted()) + .andReturn(); + + this.mockMvc.perform(asyncDispatch(mvcResult)) + .andDo(print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("data", is("bar"))) + .andReturn(); + + assertThat(captor.getAllValues().size(), is(1)); + + assertThat(captor.getValue().getQuery(), is(query)); + + } + + @Test + public void testQueryParamPostRequest() throws Exception { + String variablesJson = "{\"variable\":\"variableValue\"}"; + String query = "query myQuery {foo}"; + String operationName = "myQuery"; + + ExecutionResultImpl executionResult = ExecutionResultImpl.newExecutionResult() + .data("bar") + .build(); + CompletableFuture cf = CompletableFuture.completedFuture(executionResult); + ArgumentCaptor captor = ArgumentCaptor.forClass(ExecutionInput.class); + Mockito.when(graphql.executeAsync(captor.capture())).thenReturn(cf); + + + MvcResult mvcResult = this.mockMvc.perform(post("/graphql") + .param("query", query) + .param("variables", variablesJson) + .param("operationName", operationName)) + .andExpect(status().isOk()) + .andExpect(request().asyncStarted()) + .andReturn(); + + this.mockMvc.perform(asyncDispatch(mvcResult)) + .andDo(print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("data", is("bar"))) + .andReturn(); + + assertThat(captor.getAllValues().size(), is(1)); + + Map variables = new LinkedHashMap<>(); + variables.put("variable", "variableValue"); + assertThat(captor.getValue().getQuery(), is(query)); + assertThat(captor.getValue().getVariables(), is(variables)); + assertThat(captor.getValue().getOperationName(), is(operationName)); + + } + + @Test + public void testSimpleQueryParamPostRequest() throws Exception { + Map request = new LinkedHashMap<>(); + String query = "{foo}"; + request.put("query", query); + + ExecutionResultImpl executionResult = ExecutionResultImpl.newExecutionResult() + .data("bar") + .build(); + CompletableFuture cf = CompletableFuture.completedFuture(executionResult); + ArgumentCaptor captor = ArgumentCaptor.forClass(ExecutionInput.class); + Mockito.when(graphql.executeAsync(captor.capture())).thenReturn(cf); + + + MvcResult mvcResult = this.mockMvc.perform(post("/graphql") + .param("query", query)) + .andExpect(status().isOk()) + .andExpect(request().asyncStarted()) + .andReturn(); + + this.mockMvc.perform(asyncDispatch(mvcResult)) + .andDo(print()).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("data", is("bar"))) + .andReturn(); + + assertThat(captor.getAllValues().size(), is(1)); + + assertThat(captor.getValue().getQuery(), is(query)); + + } + + @Test + public void testApplicationGraphqlPostRequest() throws Exception { + String query = "{foo}"; + + ExecutionResultImpl executionResult = ExecutionResultImpl.newExecutionResult() + .data("bar") + .build(); + CompletableFuture cf = CompletableFuture.completedFuture(executionResult); + ArgumentCaptor captor = ArgumentCaptor.forClass(ExecutionInput.class); + Mockito.when(graphql.executeAsync(captor.capture())).thenReturn(cf); + + + MvcResult mvcResult = this.mockMvc.perform(post("/graphql") + .content(query) + .contentType("application/graphql")) .andExpect(status().isOk()) .andExpect(request().asyncStarted()) .andReturn(); @@ -206,4 +307,4 @@ public void testSimpleGetRequest() throws Exception { } -} \ No newline at end of file +}