Summary

  • When using @ExceptionHandler (or @ControllerAdvice), exceptions thrown while writing to a StreamingResponseBody are not handled correctly (or at least as I expect them to be...) when the request mapping returns ResponseEntity<StreamingResponseBody>
  • Exception handling does work as expected when the return type is just StreamingResponseBody
  • It's possible my understanding of how these should work is incorrect, however I haven't seen any evidence yet to point to this

Description

  • If a runtime exception occurs while writing to the StreamingResponseBody wrapped in a ResponseEntity the endpoint does invoke the @ExceptionHandler but returns a 500 status with no response body no matter what
  • If the request mapping returns a StreamingResponseBody directly, error handling does work as expected, and the response code and response body are correct
  • While writing tests for this, I can't get the tests to return anything other than 200 status. While running tests, I also don't see the exception handlers getting invoked. I'm not sure if this is another bug or if my test setup is incorrect. If you hit the endpoints directly in the browser or via CURL, you do see the 500 status with no body. This is my first time using StreamingResponseBody so I apologize if I'm just doing things incorrectly
  • Sample project to reproduce: https://github.com/austinarbor/spring-boot-streaming-response-issue

Misc

Comment From: vforchi

I have a similar issue: the application works as expected, no matter if I wrap the StreamingResponseBody in a ResponseEntity or not, but in the tests I always get a 200 and an empty body. I tried with @WebMvcTest+mockMvc and with @SpringBootTest+TestRestTemplate: same behaviour.

Comment From: rstoyanchev

The methods that return ResponseEntity set the content type to "text/csv" so the exception handler later fails to write the ErrorDetails with that content type. I see:

2020-12-01 21:18:59.916  INFO 331037 --- [  XNIO-1 task-1] dev.aga.controller.SomeController        : Inside handleIllegalArgumentException
2020-12-01 21:19:00.788  WARN 331037 --- [  XNIO-1 task-1] .m.m.a.ExceptionHandlerExceptionResolver : Failure in @ExceptionHandler dev.aga.controller.SomeController#handleIllegalArgumentException(IllegalArgumentException, HttpServletRequest, HttpServletResponse)

org.springframework.http.converter.HttpMessageNotWritableException: No converter for [class dev.aga.model.ErrorDetails] with preset Content-Type 'text/csv'

You'll need to also set the content type on the ResponseEntity of the error handler method. However I found out that doesn't work because the ServletServerHttpResponse wrapper does not propagate header changes to the underlying response until it writes the body, and yet it looks up values in it. We'll need to fix that.

In the mean time you can work around this by setting the content type directly on the response:

@ExceptionHandler(IllegalArgumentException.class)
ResponseEntity<ErrorDetails> handleIllegalArgumentException(IllegalArgumentException e, HttpServletRequest request, HttpServletResponse response) {
    log.info("Inside handleIllegalArgumentException");
    var errorDetails = new ErrorDetails(request.getRequestURI(), e.getMessage());
    response.setContentType("application/json");
    return ResponseEntity.status(HttpStatus.BAD_GATEWAY)
            .contentType(MediaType.APPLICATION_JSON)
            .body(errorDetails);
}

Comment From: rstoyanchev

As for the tests, MockMvc doesn't perform async dispatches, so all you see is the initial handling up until the point the async result is produced. You can perform the async dispatches manually. This is described in the docs. However I would also recommend using the WebTestClient support with MockMvc that's new 5.3

Comment From: austinarbor

Hi @rstoyanchev thanks for you response. Based on your info, I think these are the changes required to make everything work: https://github.com/austinarbor/spring-boot-streaming-response-issue/pull/1/files

Comment From: rstoyanchev

@austinarbor yes that would be the workaround for the @ExceptionHandler method. Also the test updates look about right from a quick look.