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