Spring Boot RestTemplate: test GET request with JSON body is not working
I’m building a test for a Spring GET controller. The endpoint accepts a JSON body to avoid a list of parameters (i.e. query parameters).
When I try to build the test in Spring I get this error:
HttpMessageNotReadableException: Required request body is missing
INFO 21697 --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
WARN 21697 --- [o-auto-1-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public org.springframework.http.ResponseEntity<java.lang.String> dev.marco.example.springboot.FeatureController.getSum(dev.marco.example.springboot.model.OperationValues) throws org.json.JSONException]
This example code should give you an idea of the original issue:
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
JSONObject parameters = new JSONObject();
parameters.put("valueA", 1);
parameters.put("valueB", 2);
RequestEntity requestEntity = new RequestEntity(parameters.toString(), headers, HttpMethod.GET, URI.create("http://localhost:" + port + "/feature"));
ResponseEntity<String> responseEntity =
restTemplate.exchange(requestEntity, String.class);
Assertions.assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
Assertions.assertThat(responseEntity.getBody()).isEqualTo("{\"result\":3}");
The exchange
method throws an error and return 400 BAD_REQUEST
.
The GET Spring Boot mapping is nothing complicated:
@GetMapping(value = "/feature", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<String> getSum(@RequestBody OperationValues operationValues) throws JSONException {
Integer sumResult = featureService.getSum(operationValues.getValueA(), operationValues.getValueB());
JSONObject result = new JSONObject();
result.put("result", sumResult);
return ResponseEntity.ok(result.toString());
}
This example simply accepts a JSON that contains 2 values and returns the result of a mathematical operation.
public class OperationValues {
private Integer valueA;
private Integer valueB;
...
}
If I call the method from an external application
curl -X GET --location "http://localhost:8080/feature" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d "{\"valueA\":5, \"valueB\":7}"
I receive an HTTP 200 answer with the correct answer.
Even more interesting if I change the @GetMapping
in @PostMapping
and in the test I call use HttpMethod.POST
in place of HttpMethod.GET
creating a POST request ... everything works correctly.
Origin of the issue
The problem is probably originated from the HTTP/1.1 specification that allows the servers to reject the payload in the GET request messages because it has no defined semantic.
In Spring when you use the RestTemplate
in your test a default HttpURLConnection
is prepared in SimpleClientHttpRequestFactory
and the GET
method set the doOutput
flag to false.
private void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException {
...
boolean mayWrite =
("POST".equals(httpMethod) || "PUT".equals(httpMethod) ||
"PATCH".equals(httpMethod) || "DELETE".equals(httpMethod));
connection.setDoInput(true);
connection.setInstanceFollowRedirects("GET".equals(httpMethod));
connection.setDoOutput(mayWrite);
...
}
The doOutput
according to the Spring documentation:
A URL connection can be used for input and/or output. Set the doOutput flag to true if you intend to use the URL connection for output, false if not. The default is false.
This flag is used during the test by SimpleBufferingClientHttpRequest
that build the Http answer.
In it’s method executeInternal
private ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {
addHeaders(this.connection, headers);
...
if (this.connection.getDoOutput() && this.outputStreaming) { // marco: this is skipped
this.connection.setFixedLengthStreamingMode(bufferedOutput.length);
}
this.connection.connect();
if (this.connection.getDoOutput()) { // marco: this is skipped
FileCopyUtils.copy(bufferedOutput, this.connection.getOutputStream());
} else {
// Immediately trigger the request in a no-output scenario as well
this.connection.getResponseCode(); // marco: it exits here
}
return new SimpleClientHttpResponse(this.connection);
}
The output of the connection is skipped and a 400 response code is returned directly (no-output scenario).
As a developer, if you have to test a GET message you can use an alternative to RestTemplate
(e.g. @MockMVC
a post will follow) or build your own RequestFactory
as shown in this post on StackOverflow