Boot 3 升级后错误响应正文发生更改

Ger*_*oza 4 spring spring-mvc spring-boot

我的项目中有以下控制器端点:

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

    return toDto(model);
}
Run Code Online (Sandbox Code Playgroud)

当我的应用程序找不到Foo具有提供的 id 的响应时,它检索到以下响应:

{
    "timestamp": "2023-01-06T08:43:12.161+00:00",
    "status": 404,
    "error": "Not Found",
    "path": "/foo/99"
}
Run Code Online (Sandbox Code Playgroud)

但是,升级到 Boot 3 后,响应正文更改为:

{
    "type": "about:blank",
    "title": "Not Found",
    "status": 404,
    "instance": "/foo/99"
}
Run Code Online (Sandbox Code Playgroud)

我在《Spring Boot 3.0 迁移指南》《升级到 Spring Framework 6.x 》中都找不到任何相关信息。

Ger*_*oza 13

Spring Web 6 引入了对“HTTP API 问题详细信息”规范RFC 7807 的支持。

这样,ResponseStatusException现在实现了ErrorResponse接口并扩展了ErrorResponseException类。

快速浏览一下 javadoc,我们可以看到所有这些都由 RFC 7807 格式的ProblemDetail正文支持,正如您可以想象的那样,它具有您收到的新响应的字段,并且还使用application/problem+json中的媒体类型响应。

这里是 Spring 现在如何处理错误响应的参考,这自然会朝着全面使用问题详细信息规范的方向发展

现在,通常情况下,如果您只是简单地依赖引导的错误处理机制而不进行任何进一步的更改,您仍然会看到与以前相同的响应。我的猜测是您正在使用@ControllerAdvice扩展ResponseEntityExceptionHandler. 这样,您就可以启用 RFC 7807(根据此处的Spring 文档)

所以,这就是你ResponseStatusException改变其正文内容的原因。

配置问题详细信息响应正文以包含以前的字段

如果您需要坚持使用预先存在的字段(至少在您完全迁移到基于问题详细信息的方法之前),或者如果您只想将自定义字段添加到错误响应中,则可以重写扩展类createResponseEntity中的方法,如下所示:@ControlAdviceResponseEntityExceptionHandler

@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);
    }
}

Run Code Online (Sandbox Code Playgroud)

注意:我使用new Date()Java Time 而不是 Java Time,只是因为 BootDefaultErrorAttributes类使用的是 Java Time。另外,为了简单起见,我没有使用 Boot ErrorAttributes

请注意,定义path字段有点棘手,因为在这个阶段problemDetail.getInstance()返回;null该框架稍后在HttpEntityMethodProcessor.

当然,这个解决方案适用于 servlet 堆栈,但它也应该有助于弄清楚如何在反应式堆栈中继续进行。

这样,响应将如下所示:

{
    "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"
}
Run Code Online (Sandbox Code Playgroud)

当然,它有重复的字段。如果您愿意,可以完全替换方法中的响应正文。

最后,这里有一个重要的建议:你必须有意识地实现这个逻辑;错误响应不应该用作调试工具(至少不应该在生产环境中 - 这就是 Boot 默认情况下禁用公开错误信息的原因),因为它可能会产生安全隐患(泄漏有关实现内部的信息,这可能是可能被利用)。

正如 RFC 7807 规范所指出的,它应该被用作“公开有关 HTTP 接口本身的更多细节的一种方式”。

自定义问题详细信息响应,并显示引发的异常

正如我们所理解的,通过上面使用的机制,我们无法访问原始的Exception,有时可能需要原始的 来检索具有指定问题详细信息格式的合适结果。

下面是一个示例,说明如何利用这些ResponseEntityExceptionHandler功能来实现这一目标。例如,在本例中,我添加一个带有message错误消息的附加成员和一个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;
    }
}
Run Code Online (Sandbox Code Playgroud)

将 Boot 配置为也使用问题详细信息规范

Boot 尚未提供在其错误处理机制中使用问题详细信息规范的支持 - 在某些时候,它会提供支持,如本期所述

但是,我们可以轻松地使用应用程序属性来指示,以启用 Spring MVC 提供的支持,以使用问题详细信息规范格式来处理它引发的异常(与我们包含 a 时的情况相同ResponseEntityExceptionHandler ControllerAdvice):

spring.mvc.problemdetails.enabled=true
Run Code Online (Sandbox Code Playgroud)

需要明确的是,这并没有实现 Spring MVC 框架未引发的其他异常的规范;对于这些错误,引导错误处理机制仍将检索其旧ErrorAttributes 格式,从而对不同类型的错误产生不一致的响应。

将 @ExceptionHandler 响应委托给 Boot 错误处理模型

实际上,严格来说这并不是 Boot 3 的更改,但值得在现阶段提出。

当我们实现一个@ExceptionHandler逻辑时,它将完全控制我们检索的响应。Boot 3 确实允许轻松检索 ProblemDetail 格式的响应(通过检索或其实现类ErrorResponse,如现在的)​​,但尚不清楚如何操作响应,并且仍然依赖于传统的 Boot 错误处理逻辑。ProblemDetailResponseStatusException

/error现在,如果我们了解它是如何工作的,并意识到 Boot在到达逻辑之前确实为其端点准备了错误处理信息@ExceptionHandler,那么我们可以使用它来驱动对此机制的响应:

@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;
}
Run Code Online (Sandbox Code Playgroud)

注意:如果我们@ExceptionHandler不在 a 中声明它@RestController,那么我们可以简单地检索/errorString 而不必创建一个ModelAndView实例。