如何在Spring WebFlux中避免处理AbortedException并保留通用异常处理?
我们的大型Spring WebFlux项目依赖@ExceptionHandler/@RestControllerAdvice实现全局异常处理,已配置针对Exception.class的兜底逻辑:
@ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public RestError handleUnknown(Exception exception) { log.error(Optional.ofNullable(exception.getCause()) .map(Throwable::getMessage) .orElse(exception.getMessage()), exception); return new RestError(exception); }
该兜底逻辑仅作为偶尔触发的 fallback,我们已覆盖大部分特定异常的处理。但遇到Netty的AbortedException时,异常处理器尝试写入响应体,日志会出现模糊错误:
[xxx] Error [java.lang.UnsupportedOperationException] for HTTP GET "/actuator/health/readiness", but ServerHttpResponse already committed (200 OK)
通过自定义WebFilter添加SignalListener打印堆栈后发现,实际错误为Connection has been closed BEFORE send operation,写入响应体时设置Content-Length头因Headers为只读抛出UnsupportedOperationException:
java.lang.UnsupportedOperationException: null at org.springframework.http.ReadOnlyHttpHeaders.set(ReadOnlyHttpHeaders.java:108) Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: Error has been observed at the following site(s): *__checkpoint ⇢ Exception handler com.company.common.exception.handler.RestExceptionHandler#handleUnknown(Exception), error="Connection has been closed BEFORE send operation" [DispatcherHandler] *__checkpoint ⇢ com.company.common.monitoring.logging.raw.LoggingWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ com.breakwater.sitecheckerservice.common.config.ErrorLoggingWebFilter [DefaultWebFilterChain] *__checkpoint ⇢ HTTP POST "/v1/some-url" [ExceptionHandlingWebHandler] Original Stack Trace: at org.springframework.http.ReadOnlyHttpHeaders.set(ReadOnlyHttpHeaders.java:108) at org.springframework.http.HttpHeaders.setContentLength(HttpHeaders.java:963) at org.springframework.http.codec.EncoderHttpMessageWriter.lambda$write$1(EncoderHttpMessageWriter.java:135)
需求:让兜底@ExceptionHandler要么不处理AbortedException,要么不返回任何响应,或在特定情况下告知框架无需写入响应,同时正常处理其他Exception类型。
方案1:单独匹配AbortedException,跳过响应写入
Spring异常处理器会优先匹配最具体的异常类型,添加一个针对AbortedException的处理器,直接返回空响应即可跳过兜底逻辑:
@ExceptionHandler(AbortedException.class) public Mono<Void> handleAbortedException(AbortedException ex) { // 可选:记录调试日志,无需告警 log.debug("Client connection aborted, skip response writing", ex); // 返回empty表示不写入任何响应体 return Mono.empty(); }
注意:AbortedException是Netty类,全类名需根据Netty版本调整,常见为io.netty.channel.AbortedException或io.netty.handler.codec.http2.Http2Exception$AbortedException。
方案2:在兜底处理器中判断异常类型,分支处理
修改原有兜底处理器,递归检查异常链中是否包含AbortedException,若是则返回空响应,否则正常处理:
@ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Mono<Object> handleUnknown(Exception exception) { if (containsAbortedException(exception)) { log.debug("Connection aborted, skip response", exception); return Mono.empty(); } log.error(Optional.ofNullable(exception.getCause()) .map(Throwable::getMessage) .orElse(exception.getMessage()), exception); return Mono.just(new RestError(exception)); } // 辅助方法:递归检查异常链 private boolean containsAbortedException(Throwable throwable) { if (throwable == null) { return false; } // 用类名模糊匹配兼容不同Netty版本 if (throwable.getClass().getName().contains("AbortedException")) { return true; } return containsAbortedException(throwable.getCause()); }
使用Mono<Object>作为返回类型,既可以返回业务错误对象,也能返回Mono.empty()表示无响应。
方案3:判断响应是否已提交,通用跳过逻辑
如果需要覆盖所有响应已提交的场景(不止AbortedException),可以注入ServerWebExchange,检查响应状态后决定是否处理:
@ExceptionHandler(Exception.class) public Mono<Object> handleUnknown(Exception exception, ServerWebExchange exchange) { ServerHttpResponse response = exchange.getResponse(); if (response.isCommitted()) { log.debug("Response already committed, skip exception handling", exception); return Mono.empty(); } log.error(Optional.ofNullable(exception.getCause()) .map(Throwable::getMessage) .orElse(exception.getMessage()), exception); response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); return Mono.just(new RestError(exception)); }
这里去掉@ResponseStatus注解,改为手动设置状态码,避免在响应已提交时尝试修改响应头或状态码。
内容的提问来源于stack exchange,提问作者62mkv

