Spring Web 6 introduced support for the "Problem Details for HTTP APIs" specification, RFC 7807.
With this, the ResponseStatusException now implements the ErrorResponse interface and extends the ErrorResponseException class.
Having a quick look at the javadocs, we can see that all these are backed by the RFC 7807 formatted ProblemDetail body, which, as you can imagine, has the fields of the new response you're getting, and also uses the application/problem+json
media type in the response.
Here is a reference to how Spring now treats Error Responses, which naturally goes in the direction of using Problem Details spec across the board.
Now, normally, if you were simply relying on Boot's Error Handling mechanism without any further change, you would still see the same response as before. My guess is that you are using a @ControllerAdvice
extending ResponseEntityExceptionHandler
. With that, you are enabling RFC 7807 (as per the Spring docs here)
So, that is why your ResponseStatusException
has changed its body content.
Configuring the Problem Detail response body to include previous fields
If you need to stick to the pre-existing fields (at least until you fully migrate to the Problem Details based approach) or if you simply want to add custom fields to the error response, you can override the createResponseEntity
method in the @ControlAdvice
class extending ResponseEntityExceptionHandler
as follows:
@ControllerAdvice
public class CustomExceptionsHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> createResponseEntity(@Nullable Object body, HttpHeaders headers, HttpStatusCode statusCode, WebRequest request) {
if (body instanceof ProblemDetail) {
ProblemDetail problemDetail = ((ProblemDetail) body);
problemDetail.setProperty("error", problemDetail.getTitle());
problemDetail.setProperty("timestamp", new Date());
if (request instanceof ServletWebRequest) {
problemDetail.setProperty("path", ((ServletWebRequest) request).getRequest()
.getRequestURI());
}
}
return new ResponseEntity<>(body, headers, statusCode);
}
}
Note: I'm using new Date()
instead of Java Time simply because that's what Boot's DefaultErrorAttributes
class uses. Also, I'm not using Boot's ErrorAttributes
for simplicity.
Note that defining the path
field is a little bit tricky because problemDetail.getInstance()
returns null
at this stage; the framework sets it up later in the HttpEntityMethodProcessor
.
Of course, this solution is suitable for a servlet stack, but it should help figure out how to proceed in a reactive stack as well.
With this, the response will look as follows:
{
"type": "about:blank",
"title": "Not Found",
"status": 404,
"instance": "/foo/99",
"error": "Not Found",
"path": "/foo/99",
"timestamp": "2023-01-06T10:00:20.509+00:00"
}
Of course, it has duplicated fields. You can completely replace the response body in the method if you prefer.
Finally, an important piece of advise here: You have to implement this logic consciously; the error responses shouldn't be used as a debugging tool (at least not in a production environment - that's why Boot disables exposing the error information by default), since it can have security implications (leaking information on the implementation internals, that can be potentially exploited).
As indicated by the RFC 7807 spec, it should be used as "a way to expose greater detail about the HTTP interface itself".
Customizing the Problem Details response with visibility on the raised Exception
As we can appreciate, with the mechanism used above, we don't have access to the original Exception
, which might be needed at times to retrieve a suitable result with the specified Problem Details format.
Here is an example of how you can make use of the ResponseEntityExceptionHandler
capabilities to achieve this. In this case, for example, I am adding an additional message
member with the error message and an errors
field containing the input validation errors:
@ControllerAdvice
public class CustomExceptionsHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleExceptionInternal(Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatusCode statusCode, WebRequest request) {
ResponseEntity<Object> response = super.handleExceptionInternal(ex, body, headers, statusCode, request);
if (response.getBody() instanceof ProblemDetail problemDetailBody) {
problemDetailBody.setProperty("message", ex.getMessage());
if (ex instanceof MethodArgumentNotValidException subEx) {
BindingResult result = subEx.getBindingResult();
problemDetailBody.setProperty("message", "Validation failed for object='" + result.getObjectName() + "'. " + "Error count: " + result.getErrorCount());
problemDetailBody.setProperty("errors", result.getAllErrors());
}
}
return response;
}
}
Configuring Boot to also use the Problem Detail spec
Boot doesn't yet provide support to use the Problem Detail spec in its Error Handling mechanism - at some point, it will, as indicated in this issue.
However, we can easily indicate with an application property to enable the support that Spring MVC provides to use the Problem Details spec format for the exceptions it raises (same as it does when we include a ResponseEntityExceptionHandler
ControllerAdvice
):
spring.mvc.problemdetails.enabled=true
Just to be clear, this doesn't implement the spec on other exceptions not raised by the Spring MVC framework; for those ones, the Boot Error Handling mechanism will still retrieve its old ErrorAttributes
format, producing inconsistent responses for different types of errors.
Delegating the @ExceptionHandler response to Boot error handling model
Actually, this is not strictly a change for Boot 3, but worth bringing up at this stage.
When we implement a @ExceptionHandler
logic, it will take full control of the response we retrieve. Boot 3 does allow to easily retrieve a ProblemDetail-formatted response (by retrieving an ErrorResponse
, ProblemDetail
or its implementing classes like now the ResponseStatusException
), but it's not that clear how to manipulate the response and still rely on the traditional Boot error handling logic.
Now, if we understand how it works, and realize that Boot does prepare the error handling information for its /error
endpoint before reaching the @ExceptionHandler
logic, then we can use this to drive the response to this mechanism:
@ExceptionHandler({ EntityNotFoundException.class })
public ModelAndView resolveException(HttpServletRequest request, Exception ex) {
request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, HttpStatus.BAD_REQUEST.value());
request.setAttribute(RequestDispatcher.ERROR_MESSAGE,
"This will override the error message configured by Boot");
ModelAndView mav = new ModelAndView();
mav.setViewName("/error");
return mav;
}
Note: if we're not declaring this @ExceptionHandler
within a @RestController
, then we can simply retrieve the /error
String instead of having to create a ModelAndView
instance.