Feign异常处理实践

前言

在分布式服务调用中,Feign 作为声明式 HTTP 客户端,默认会根据 HTTP 状态码判断请求是否成功。如果被调用方在全局异常处理器中将所有业务异常统一转换为 HTTP 200,并把错误信息封装在响应体中,例如:

1
2
3
4
5
6
7
@ExceptionHandler({BaseException.class})
public ResponseEntity<ServiceResponse> handleBusinessException(BaseException ex) {
return this.create(
HttpStatus.OK,
new ServiceResponse(ex.getCode(), ex.getMessage())
);
}

这实际上把业务失败伪装成了“HTTP 200 成功响应”。从调用方角度看,Feign 不会触发异常,TCP/HTTP 请求成功,导致后续逻辑误判为成功。

问题分析

1. Feign 不抛异常

OpenFeign 的默认行为是:

  • HTTP 2xx 表示请求成功,按照正常逻辑解析响应体;
  • 非 2xx 表示错误,进而触发 ErrorDecoder 或抛出异常。

当后端统一返回 HTTP 200 时,Feign 无法凭状态码判断调用是否失败,仅靠响应体字段难以统一处理。

2. 事务无法感知失败

Spring 事务和分布式事务(如 Seata)依赖于调用链中的异常传播。如果上游服务返回 HTTP 200,调用方没有显式抛出异常,事务会认为当前分支已成功提交,从而无法回滚。

3. 监控与链路追踪失真

将失败伪装成 200 会造成:

  • Sentinel 的成功/失败统计失真;
  • SkyWalking 的链路错误率无法正确反映;
  • Prometheus 的 HTTP 状态码/错误率指标失效。

4. 责任边界模糊

业务异常与传输层异常混淆后,调用方无法明确是“服务调用失败”还是“业务处理失败”,这会增加故障排查成本。

典型风险场景

很多项目为了统一响应格式,采用如下返回:

1
2
3
4
5
{
"code": 1001,
"msg": "业务异常",
"success": false
}

如果调用方未做统一校验:

1
2
3
if (!response.success()) {
throw new BusinessException(...);
}

则该异常信息只停留在业务层面,整个远程调用链仍被视为成功。

最佳实践

1. 服务端异常返回非 2xx

RESTful 风格下,业务异常与系统异常应区分 HTTP 状态码:

  • 4xx:参数校验、权限异常、业务约束异常等客户端错误;
  • 5xx:系统异常、服务内部错误等服务器错误;
  • 2xx:表示请求处理成功。

服务端统一异常处理器应遵循该语义:

1
2
3
4
5
@ExceptionHandler(BaseException.class)
public ResponseEntity<ServiceResponse> handleBusinessException(BaseException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ServiceResponse(ex.getCode(), ex.getMessage(), false));
}

如果有兼容性要求无法立即改造,也应尽量保留 code/success 字段,并在调用方做二次解码。

2. 在 Feign 端统一解码和异常转换

推荐在 Feign 客户端增加统一的 DecoderErrorDecoder

  • 2xx 响应体中携带的业务失败标志进行检查;
  • 4xx/5xx 响应体进行统一解析并抛出业务异常;
  • 让调用方只关注异常语义,而不是手动判断 success

示例配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class FeignConfig {

@Bean
public Decoder feignDecoder() {
return new FeignResultDecoder(new JacksonDecoder());
}

@Bean
public ErrorDecoder errorDecoder() {
return new FeignErrorDecoder(new JacksonDecoder());
}
}

FeignResultDecoder 的核心逻辑通常是:

  • 先按照统一响应结构解析;
  • success == falsecode != 0,则抛出 BusinessException
  • 否则返回真实业务数据。

3. 调用方仍应保留一致性校验

即便使用统一解码器,业务调用处也应遵循“幂等、失败显式处理”的原则。

1
2
3
4
ServiceResponse<T> response = feignClient.call(...);
if (!response.success()) {
throw new BusinessException(response.getCode(), response.getMsg());
}

该方式适用于兼容旧系统,作为“防御式编程”的补充,但不应依赖于它作为默认异常传播机制。

4. 明确统一响应结构与异常规则

推荐制定统一接口规范:

  • success 字段仅表示业务处理结果;
  • code 字段表示业务错误码;
  • message 表示错误原因;
  • data 表示业务数据;
  • HTTP 状态码仍然遵循语义,不因业务失败而全部返回 200。

这样可以让监控、链路追踪和容错组件共同工作。

代码示例

Feign Result Decoder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class FeignResultDecoder implements Decoder {

private final Decoder delegate;

public FeignResultDecoder(Decoder delegate) {
this.delegate = delegate;
}

@Override
public Object decode(Response response, Type type) throws IOException, FeignException {
if (response.status() != 200) {
return delegate.decode(response, type);
}

// 先按统一响应结构解析
ResponseBody body = response.body();
ServiceResponse<?> result = parse(body, ServiceResponse.class);
if (result == null) {
return delegate.decode(response, type);
}

if (!result.isSuccess()) {
throw new BusinessException(result.getCode(), result.getMsg());
}

return result.getData();
}
}

服务端异常处理建议

1
2
3
4
5
@ExceptionHandler(BaseException.class)
public ResponseEntity<ServiceResponse<Object>> handleBusinessException(BaseException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ServiceResponse<>(ex.getCode(), ex.getMessage(), false));
}

永远返回 HTTP 200 的负面影响

1. Sentinel 统计失真

Sentinel 依赖异常、响应时间和状态码来判断请求是否失败。持续返回 200 会导致错误率统计偏低,规则判定失效。

2. SkyWalking 链路错误率失真

SkyWalking 默认将 HTTP 5xx、RPC 异常等判定为链路错误。若所有异常都被屏蔽成 200,链路分析会误导运维人员认为调用链路健康。

3. Prometheus 指标失真

Prometheus 常规告警规则依赖 HTTP statussuccesserror 指标。当失败不再映射到正确状态码时,告警门槛失效,故障发现被延迟。

4. 分布式事务回滚失效

Seata 等分布式事务框架依赖调用链异常传播。调用方收不到异常,则无法触发 TCC/RM 回滚,最终导致脏数据。

结论

Feign 调用链中,HTTP 状态码与业务响应语义必须分层处理。绝大多数情况下,业务失败应返回非 2xx 状态码;如果因兼容性需要暂时保留 200,则必须在 Feign 端统一解码并转换为异常。

关键点:

  • HTTP 200 只是传输成功,不代表业务成功;
  • 业务异常应由服务端明确状态码并由调用端统一抛出;
  • 监控与分布式事务依赖正确的异常传播。