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

如何在Spring WebFlux中避免处理AbortedException并保留通用异常处理?

问题:Spring WebFlux中AbortedException触发兜底异常处理器导致的Headers只读错误

我们的大型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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 22:27:08