Error Response body changed after Boot 3 upgrade
Asked Answered
N

1

7

I have the following Controller endpoint in my project:

@GetMapping(value = "/{id}")
public FooDto findOne(@PathVariable Long id) {
    Foo model = fooService.findById(id)
        .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));

    return toDto(model);
}

My application retrieved the following response when it couldn't find a Foo with the provided id:

{
    "timestamp": "2023-01-06T08:43:12.161+00:00",
    "status": 404,
    "error": "Not Found",
    "path": "/foo/99"
}

However, after upgrading to Boot 3, the response body changed to:

{
    "type": "about:blank",
    "title": "Not Found",
    "status": 404,
    "instance": "/foo/99"
}

I couldn't find any related information in the Spring Boot 3.0 Migration Guide nor in the Upgrading to Spring Framework 6.x one.

Nutwood answered 6/1, 2023 at 10:46 Comment(0)
N
19

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.

Nutwood answered 6/1, 2023 at 10:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.