The spring-boot produces very convinient default JSON response for exceptions, like this.
{
"timestamp" : "2016-11-06T03:16:02.440+0000",
"status" : 500,
"error" : "Internal Server Error",
"exception" : "demo.MyException",
"message" : "bla bla",
"path" : "/api/data"
}
When I write my exception handler like this:
@ExceptionHandler
@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "bla bla")
public void handleMyException(MyException e) {
}
Then HTTP response includes spring-boot's JSON with error details. As expected.
But If I remove the reason
annotation argument to make use of exception message, like this:
@ExceptionHandler
@ResponseStatus(code = HttpStatus.NOT_FOUND)
public void handleMyException(MyException e) {
}
Then HTTP response is no more includes spring-boot's JSON with error details. It has empty body instead.
Maybe it is related to #5774.
Comment From: bclozel
The body of your second handleMyException
variant is empty and I see nowhere a response body being set. Spring Boot does help with nice error messages when it can, but in this case you're explicitly handling the error and not writing any body.
In my opinion, this is the expected behavior.
Could you elaborate a bit more on why you think there should be a response body here?
Comment From: xak2000
The body of my first handleMyException
variant is also empty. The only difference is the reason
attribute of @ResponseStatus
annotation.
So why it is inconsistent behaviour?
I use the void
variant of @ExceptionHandler
method and without HttpServletResponse
argument, so I expect that my code just change the response status, but not the default spring-boot's JSON response.
If I add the @ResponseStatus(code = HttpStatus.NOT_FOUND)
annotation (even without reason
) to the exception class, then spring-boot still sends it's JSON. But what should I do if I can't add this annotation to the exception class (for example, the exception is on another application layer which should not know about web layer)? The @ExceptionHandler
is for this, right? But how can I change just response status, without changing default JSON response?
The workaround now is:
@ExceptionHandler
public void handleMyException(MyException e, HttpServletResponse res) throws IOException {
res.sendError(HttpStatus.NOT_FOUND.value());
}
This code just changes the status, but still sends default spring-boot JSON with exception's error message.
But my main point is still: why it is inconsistent behaviour when we use @ExceptionHandler
in combination with @ResponseStatus
, depending on whether the reason
attribute is set or not?
The @ResponseStatus
's documentation says:
Warning: when using this annotation on an exception class, or when setting the reason attribute of this annotation, the
HttpServletResponse.sendError
method will be used.
But why the HttpServletResponse.sendError
method not used when reason
is not set? If it is to allow the default container error page to show, then why not allow this page to show when the reason
is set? Why the reason
attribute controls whether we send error or just set the response status?
I don't see why this inconsistency exists.
Comment From: bclozel
Indeed, something may not be right here, let me look a bit into this.
Just a few pointers to add to the discussion:
- Spring MVC's ResponseStatusExceptionResolver
does use sendError
in all cases (with or without the reason
attribute)
- This looks a lot like #3623, although that one is now covered by unit tests
Comment From: bclozel
When added on an Exception class, @ResponseStatus(status)
and @ResponseStatus(status, reason)
will both trigger an error dispatch to the container and will be handled by Boot's ErrorController. In this case, the ResponseStatusExceptionResolver
is involved and we know that the response should represent the error itself.
When added on an @ExceptionHandler
annotated method, @ResponseStatus(status, reason)
will trigger an error dispatch to the container and will be handled by Boot's ErrorController, but @ResponseStatus(status)
won't. Note that in fact, this is the same as adding a @ResponseStatus
on any Controller method; the same code path is involved, in ServletInvocableHandlerMethod
.
In those cases, we assume that providing a reason
means that the handler is returning an error and that this should be dispatched as an error in the container. If reason
is not there, then we can't assume that the provided response should be dispatched as an error to the container:
- the response status could be 2xx or 3xx
- the handler could return a custom error object to be serialized as the HTTP response body and the Error Controller should not be involved there
It does feel inconsistent but at the moment I can't find a proper solution that would not break many web apps out there with perfectly valid code.
Your comments already list the two available workarounds for this:
- use the reason
attribute
- use the HttpServletResponse
directly with res.sendError(HttpStatus.NOT_FOUND.value());
Comment From: xak2000
If reason is not there, then we can't assume that the provided response should be dispatched as an error to the container
Can't we decide this based on presense of @ExceptionHandler
on the same method?
So, if it is normal Controller method, then @ResponseStatus
without reason
should just change the response status, but if if is @ExceptionHandler
method then it should sendError
.
At least it is more consistent: @ResponseStatus
on exception handlers always behave the same regardless of params. But it is backward incompatible changes of course. Maybe someone relies on behaviour of @ResponseStatus(status)
(without reason
) on @ExceptionHandler
and handles their exceptions returning 200 OK
. :)
Comment From: quaff
FYI, I created a relation issue https://github.com/spring-projects/spring-framework/issues/27156