WebClient如何实现日志记录及4xx/5xx等各类错误响应处理
问题背景
当前采用同步模式使用WebClient调用多个外部API服务。当出现连接故障、SSL错误或任意4xx/5xx错误时,外部服务可能返回携带明确错误描述的响应体;本次需同时覆盖API错误、响应体为空的通信类错误两类场景。
实现目标:当外部服务调用失败时,按规则记录响应体与状态码,向本服务的调用方返回500错误形式的异常。
现存问题
需要实现在4xx/5xx调用失败时记录外部API响应体,同时记录外部API通信相关错误(包括外部服务端口变更、SSL连接类问题等),现需确认是否可同时实现异常信息与响应体的统一处理。
示例场景
外部服务返回401状态码时,响应体如下:
{ "error": "invalid client certificate used" }
现有代码
下述现有代码完全无法展示具体错误信息:
public String callExternalAPI() { final HttpStatus[] statuses = {HttpStatus.OK}; try { String response = webClientLogon.post() .uri(builder -> builder.path(myExternalServiceURL).build()) .headers(this::myHeaders) .exchangeToMono(res ->{if (res.statusCode().equals(HttpStatus.OK)) { return res.bodyToMono(String.class); } else if (res.statusCode().isError()) { statuses[0] = res.statusCode(); return res.bodyToMono(String.class); } else { statuses[0] = res.statusCode(); return res.bodyToMono(String.class); }}) .block(); return handleResponse(response, statuses[0]); } catch (WebClientException ex){ throw handleWebClientException(ex); } }
实现方案
完全可以实现两类错误的统一处理,不需要用单元素数组中转存储状态码,直接在响应式流内完成分支判断即可,核心逻辑如下:
- 在
exchangeToMono阶段统一读取所有响应的body内容,通过defaultIfEmpty处理空响应场景,避免无响应体时抛出额外异常 - 对2xx成功响应直接返回body内容,对4xx/5xx错误响应,直接组装携带状态码、响应体的自定义异常抛出,不将错误状态的响应传递到后续流程
- 外层catch捕获所有
WebClientException,既包含前面主动抛出的携带响应体的API调用异常,也包含连接失败、SSL错误、超时等无响应体的通信类异常,在catch块中统一完成日志记录、异常转换逻辑
修正后代码
// 自定义外部API调用异常,用于携带状态码和错误响应体 public class ExternalApiCallException extends RuntimeException { private final int httpStatus; private final String errorBody; public ExternalApiCallException(int httpStatus, String errorBody, String message) { super(message); this.httpStatus = httpStatus; this.errorBody = errorBody; } public int getHttpStatus() { return httpStatus; } public String getErrorBody() { return errorBody; } } public String callExternalAPI() { try { return webClientLogon.post() .uri(builder -> builder.path(myExternalServiceURL).build()) .headers(this::myHeaders) .exchangeToMono(clientResponse -> { HttpStatus status = clientResponse.statusCode(); // 统一读取响应体,空响应默认赋值为空字符串 return clientResponse.bodyToMono(String.class) .defaultIfEmpty("") .flatMap(responseBody -> { if (status.is2xxSuccessful()) { return Mono.just(responseBody); } // 错误状态码直接抛出携带完整上下文的异常 return Mono.error(new ExternalApiCallException( status.value(), responseBody, String.format("外部API调用返回错误状态:%d", status.value()) )); }); }) .block(); } catch (WebClientException ex) { // 统一错误处理逻辑 if (ex instanceof ExternalApiCallException apiEx) { // 处理4xx/5xx类有响应的错误,记录状态码+响应体 log.error("外部API调用失败,状态码:{},响应体:{}", apiEx.getHttpStatus(), apiEx.getErrorBody(), ex); } else { // 处理连接、SSL、超时类无响应的通信错误,直接记录异常信息 log.error("外部API通信失败,错误信息:{}", ex.getMessage(), ex); } // 统一转换为本服务的500错误返回给上游调用方 throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "外部服务调用异常", ex); } }
注意事项
- 不要依赖数组、原子类等外部变量在响应式流里存状态,这种写法在并发场景下会出现状态串扰的问题,所有上下文都应该在响应式流内部传递
- 读取错误响应体时必须处理空值场景,部分通信错误、网关拒绝的场景不会返回响应体,不做空处理会触发额外的序列化异常
- 不需要单独配置WebClient的全局错误处理,在业务调用点统一处理更适配单接口的日志、异常转换需求
内容的提问来源于stack exchange,提问作者Lisa
相关产品推荐
相关产品推荐

