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

Spring WebClient使用Retry.withThrowable时非429异常被吞致无限等待问题

Spring WebClient 429重试逻辑引发非429异常无限等待的修复方案

问题背景

使用Spring WebClient实现429(请求过多)状态码的重试逻辑:提取响应头Retry-After的值,延迟对应时间后重试。当前实现对429场景有效,但遇到500等非429错误时,应用会无限等待。注释掉.retryWhen(retryAfterFixedDelay())后,异常可正常抛出,推测是该重试逻辑抑制了WebClientResponseException。

原代码示例

public SomeObject getSomeObject(int id) {
    return apiClient.get()
            .uri("/some-object/{id}", id)
            .retrieve()
            .onStatus(status -> status.equals(TOO_MANY_REQUESTS), this::throwRateLimitException)
            .bodyToMono(SomeObject.class)
            .retryWhen(retryAfterFixedDelay())
            .block();
}

private static Mono<RateLimitException> throwRateLimitException(ClientResponse response) {
    // 提取Retry-After头值逻辑...
    final RateLimitException rateLimitException = new RateLimitException(message, retryAfterSeconds);
    return Mono.error(rateLimitException);
}

private static Retry retryAfterFixedDelay() {
    return Retry.withThrowable(throwableFlux -> throwableFlux
            .filter(RateLimitException.class::isInstance)
            .flatMap(throwable -> {
                RateLimitException rateLimitException = (RateLimitException) throwable;
                log.warn(rateLimitException.getMessage());
                return Mono.delay(Duration.ofSeconds(rateLimitException.getRetryAfterSeconds()));
            }));
}

问题原因

原retryAfterFixedDelay()方法中,throwableFlux.filter(RateLimitException.class::isInstance)会过滤掉所有非RateLimitException的异常。当出现500这类错误时,过滤后的流没有元素,retryWhen会一直等待重试信号,导致应用无限挂起。

修复方案

调整重试逻辑,确保非RateLimitException的异常能被直接抛出,而非被过滤后停滞。以下是三种可行的修改方式:

方式一:直接在flatMap中区分异常类型

private static Retry retryAfterFixedDelay() {
    return Retry.withThrowable(throwableFlux -> throwableFlux
            .flatMap(throwable -> {
                if (throwable instanceof RateLimitException) {
                    RateLimitException rateLimitException = (RateLimitException) throwable;
                    log.warn(rateLimitException.getMessage());
                    return Mono.delay(Duration.ofSeconds(rateLimitException.getRetryAfterSeconds()));
                } else {
                    // 非429相关异常,直接抛出终止重试流程
                    return Mono.error(throwable);
                }
            }));
}

方式二:保留过滤+switchIfEmpty处理剩余异常

private static Retry retryAfterFixedDelay() {
    return Retry.withThrowable(throwableFlux -> throwableFlux
            .filter(RateLimitException.class::isInstance)
            .flatMap(throwable -> {
                RateLimitException rateLimitException = (RateLimitException) throwable;
                log.warn(rateLimitException.getMessage());
                return Mono.delay(Duration.ofSeconds(rateLimitException.getRetryAfterSeconds()));
            })
            // 若过滤后无元素,说明是其他异常,直接抛出
            .switchIfEmpty(Mono.defer(() -> throwableFlux.next().flatMap(Mono::error))));
}

方式三:使用Retry.from更清晰控制逻辑

private static Retry retryAfterFixedDelay() {
    return Retry.from(companion -> companion
            .filter(retrySignal -> retrySignal.failure() instanceof RateLimitException)
            .flatMap(retrySignal -> {
                RateLimitException ex = (RateLimitException) retrySignal.failure();
                log.warn(ex.getMessage());
                return Mono.delay(Duration.ofSeconds(ex.getRetryAfterSeconds()));
            }));
}

验证说明

修改后,只有RateLimitException(对应429状态)会触发延迟重试,500等其他异常会直接抛出,不会导致应用无限等待。同时保持了429场景下按Retry-After头延迟重试的逻辑。

内容的提问来源于stack exchange,提问作者Robert Strauch

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 20:55:18