You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Spring Boot中转递下游REST服务的HttpStatus异常信息

问题分析与解决方案

你遇到的核心问题是:当你的Spring Boot枢纽服务调用其他REST应用抛出HttpClientErrorException时,你希望直接将原异常的状态码和自定义消息返回给用户,但当前通过@RestControllerAdvice重新抛出异常的方式,导致testRestTemplate在解析响应时因类型不匹配报错;而你又不想用返回空响应加警告头的方案,因为这会改变用户对服务的使用预期。

为什么原方案会失败?

你原来的@RestControllerAdvice是直接重新抛出HttpClientErrorException,此时Spring会自动生成一个默认的JSON格式错误响应体(结构类似{"timestamp":"...","status":418,"error":"I'm a teapot","message":"...","path":"/1001"})。但你的测试用例中,testRestTemplate期望解析出List<MyGenericType>类型的响应体,这就导致了JSON解析异常——因为实际返回的错误体结构和预期的列表类型完全不匹配,所以抛出了HttpMessageNotReadableException。

最优解决方案:构建匹配预期的错误响应

我们不需要重新抛出异常,而是直接构建一个包含正确状态码和自定义消息的响应实体,同时返回符合客户端预期的响应体结构(比如简单的JSON错误对象或纯文本),这样既保留了原异常的状态信息,又不会破坏客户端的解析逻辑。

1. 调整@RestControllerAdvice

这里我们返回一个结构化的JSON错误响应,状态码沿用原异常的状态码,消息使用自定义内容:

@RestControllerAdvice
public class HubControllerAdvice {

    @ExceptionHandler(HttpClientErrorException.class)
    public ResponseEntity<Map<String, String>> handleHttpClientErrorException(HttpClientErrorException ex) {
        Map<String, String> errorBody = new HashMap<>();
        errorBody.put("errorMessage", "Http client says: " + ex.getMessage());
        
        return ResponseEntity.status(ex.getStatusCode())
                .contentType(MediaType.APPLICATION_JSON)
                .body(errorBody);
    }
}

如果你的客户端更接受纯文本消息,也可以改成返回文本类型:

@ExceptionHandler(HttpClientErrorException.class)
public ResponseEntity<String> handleHttpClientErrorException(HttpClientErrorException ex) {
    String errorMessage = "Http client says: " + ex.getMessage();
    return ResponseEntity.status(ex.getStatusCode())
            .contentType(MediaType.TEXT_PLAIN)
            .body(errorMessage);
}

2. 调整测试用例

因为现在服务不会抛出异常,而是返回一个带有错误状态码和响应体的ResponseEntity,所以测试需要验证响应的状态码和响应体内容,而不是断言抛出异常:

@Test
@SuppressWarnings("unchecked")
@DisplayName("should return original exception status and custom message")
void shouldReturnOriginalExceptionStatusAndCustomMessage() {
    // 模拟被调用服务抛出异常
    when(restTemplateMock.exchange(anyString(), any(), any(), any(ParameterizedTypeReference.class)))
            .thenThrow(new HttpClientErrorException(HttpStatus.I_AM_A_TEAPOT, "holy crap!"));

    // 调用枢纽服务,指定解析为错误响应的类型
    ResponseEntity<Map<String, String>> response = testRestTemplate.exchange(
            url("/1001"), 
            HttpMethod.GET, 
            null, 
            new ParameterizedTypeReference<Map<String, String>>() {}
    );

    // 验证响应结果
    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.I_AM_A_TEAPOT);
    assertThat(response.getBody()).isNotNull();
    assertThat(response.getBody().get("errorMessage")).isEqualTo("Http client says: holy crap!");
}

如果是纯文本响应,测试用例改为:

ResponseEntity<String> response = testRestTemplate.exchange(
        url("/1001"), 
        HttpMethod.GET, 
        null, 
        String.class
);
assertThat(response.getBody()).isEqualTo("Http client says: holy crap!");

方案优势

这个方案既满足了你的核心需求——将原异常的状态码和自定义消息返回给用户,又不需要改变用户的使用预期:

  • 用户仍然可以通过响应状态码判断错误类型
  • 响应体结构清晰,客户端可以正常解析
  • 完全符合REST服务的常规错误处理范式

内容的提问来源于stack exchange,提问作者jactor-rises

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 07:32:07