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

使用WebClient检测未启动服务器时无法屏蔽WARN日志的问题

解决方案

这个问题我之前处理过,核心原因是Reactor Netty的HttpClientConnect组件会在TCP连接失败/提前关闭时自动输出WARN日志,而我们之前的异常回调只覆盖了请求/响应阶段,没触达TCP连接层面的异常处理。下面是几个优雅的解决思路:

1. 从HttpClient层面拦截TCP连接异常

我们可以在tcpConfiguration里添加TCP连接阶段的异常处理,同时通过onErrorResume消化这类异常,避免内部日志触发:

HttpClient httpClient = HttpClient.create(ConnectionProvider.newConnection())
    .tcpConfiguration(tcpClient -> tcpClient
        .option(CONNECT_TIMEOUT_MILLIS, httpConnectionTimeoutMillis)
        .doOnConnected(conn -> conn
            .addHandlerLast(new ReadTimeoutHandler(httpReadTimeoutMillis, MILLISECONDS)))
        // 拦截TCP连接失败的异常
        .doOnError(ConnectException.class, e -> {
            logger.debug("Server not listening yet, TCP connection failed: {}", e.getMessage());
        })
        // 拦截连接提前关闭的异常
        .doOnError(PrematureCloseException.class, e -> {
            logger.debug("Connection closed prematurely before server response: {}", e.getMessage());
        }))
    // 优化请求错误日志,只在非连接类异常时输出WARN
    .doOnRequestError((request, e) -> {
        if (!(e instanceof PrematureCloseException || e.getCause() instanceof ConnectException)) {
            logger.warn("onRequestError: We have a request error: {}.", e.getMessage());
        }
    })
    .doOnResponseError((response, e) -> {
        logger.warn("onResponseError: We have a response error: {}.", e.getMessage());
    })
    // 关键:用onErrorResume消化连接类异常,不让它们传递到内部日志处理器
    .onErrorResume(e -> {
        if (e instanceof ConnectException || e instanceof PrematureCloseException) {
            return Mono.empty();
        }
        return Mono.error(e);
    });

2. 在WebClient请求链路中提前捕获异常

在发起请求时,通过onErrorResume精准捕获WebClient封装的连接异常,直接返回你需要的默认值,同时阻止异常向上传递:

webClient.get()
    .uri("/actuator/health")
    .retrieve()
    .bodyToMono(String.class)
    .onErrorResume(throwable -> {
        // 识别WebClient封装的连接类异常
        if (throwable instanceof WebClientRequestException) {
            Throwable cause = throwable.getCause();
            if (cause instanceof ConnectException || cause instanceof PrematureCloseException) {
                logger.debug("Server still starting, health check failed: {}", throwable.getMessage());
                // 返回你需要的默认状态,比如"DOWN"
                return Mono.just("DOWN");
            }
        }
        // 其他异常正常抛出
        return Mono.error(throwable);
    });

3. 全局拦截Reactor的异常丢弃事件

如果上面两种方式还不够,你可以通过Reactor的Hooks注册全局异常处理器,专门拦截PrematureCloseException这类不需要输出WARN的异常:

// 全局拦截异常丢弃事件,避免内部日志输出
Hooks.onErrorDropped(throwable -> {
    if (throwable instanceof PrematureCloseException || 
        (throwable instanceof ConnectException && throwable.getMessage().contains("Connection refused"))) {
        // 用DEBUG级别记录,或者直接忽略
        logger.debug("Ignoring expected connection failure during server startup: {}", throwable.getMessage());
    } else {
        // 其他异常按照默认逻辑处理
        Hooks.onErrorDropped(throwable);
    }
});

注意:全局钩子要谨慎使用,最好只在测试场景或者特定初始化流程中启用,避免影响其他正常请求的异常处理。

为什么之前的代码没生效?

你之前配置的doOnRequestError和doOnResponseError是在HTTP请求/响应阶段触发的回调,但HttpClientConnect的WARN日志是在TCP连接建立阶段产生的,这个阶段的异常没有被你的回调覆盖,所以需要在TCP层面或者全局异常处理中拦截。


内容的提问来源于stack exchange,提问作者Bert Laverman

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.08 17:28:11