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

Spring WebClient重试因ReadOnlyHttpHeaders失败问题求助

问题分析与解决方案

环境信息

java           : 21
spring-boot    : 3.2.4
spring         : 6.1.5

需求

当HTTP响应状态码非2xx时,对所有请求进行重试。

问题现象:重试时尝试写入ReadOnlyHttpHeaders

  • 服务发起初始请求到目标API,目标API返回4xx状态码。
  • 第一次重试在创建HTTP请求时失败,未到达目标API,抛出UnsupportedOperationException。
  • 第二次(最后一次)重试同样在创建HTTP请求时失败,未到达目标API,抛出UnsupportedOperationException。

原因:重试过程中HTTP请求头为ReadOnlyHttpHeaders,Spring尝试设置content-length头值时触发异常。

报错堆栈:

reactor.core.Exceptions$RetryExhaustedException: Retries exhausted: 2/2
    at reactor.core.Exceptions.retryExhausted(Exceptions.java:308)
    Suppressed: The stacktrace has been enhanced by Reactor, refer to additional information below: 
Error has been observed at the following site(s):
    *__checkpoint ⇢ Request to POST http://localhost:10000/ [DefaultWebClient]
Original Stack Trace:
        at reactor.core.Exceptions.retryExhausted(Exceptions.java:308)
        at reactor.util.retry.RetryBackoffSpec.lambda$static$0(RetryBackoffSpec.java:68)
        at reactor.util.retry.RetryBackoffSpec.lambda$null$4(RetryBackoffSpec.java:560)
        ...
Caused by: java.lang.UnsupportedOperationException
    at org.springframework.http.ReadOnlyHttpHeaders.set(ReadOnlyHttpHeaders.java:108)
    at org.springframework.http.HttpHeaders.setContentLength(HttpHeaders.java:967)
    at org.springframework.http.codec.EncoderHttpMessageWriter.lambda$write$1(EncoderHttpMessageWriter.java:135)
    at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:132)
    ... 43 more

代码实现

配置类

public class ApiClientConfiguration {
    @Bean
    public WebClient apiWebClient() {
        return WebClient.builder()
                .baseUrl("http://localhost:10000")
                .filter(filter())
                .build();
    }

    @Bean
    public ApiClient apiClient(WebClient apiWebClient) {
        return HttpServiceProxyFactory
                .builderFor(WebClientAdapter.create(apiWebClient))
                .build()
                .createClient(ApiClient.class);
    }

    private ExchangeFilterFunction filter() {
        return (request, next) ->
                next.exchange(request)
                        .flatMap(clientResponse -> {
                            if (clientResponse.statusCode().is2xxSuccessful()) {
                                return Mono.just(clientResponse);
                            }
                            return Mono.error(new RuntimeException("Not 2xx"));
                        })
                        .retryWhen(Retry.fixedDelay(2, Duration.ofSeconds(2)))
                ;
    }
}

ApiClient接口

@HttpExchange("")
public interface ApiClient {
    @PostExchange("")
    Mono<Void> sendRequest(@RequestBody RequestDto request);
}

调用代码

@Service
@RequiredArgsConstructor
public class ApiService {

    private final ApiClient apiClient;

    public Mono<Void> sendRequest(RequestDto request) {
        // Important: The retry may not appear here.
        // The method must stay as is. 
        return apiClient.sendRequest(request);
    }
}

强制要求

重试逻辑必须在ApiClientConfiguration中全局配置,不能在ApiService中设置,ApiService的方法必须保持原样,避免影响包内其他使用场景。

疑问

  1. 我的实现哪里有问题?
  2. 为什么重试时会尝试修改请求头?
  3. 有没有办法让重试时的请求头变为可写状态?
  4. 是否忽略了WebClient或重试机制的某些细节?

补充测试:通过重写Spring的HttpHeaders类,让readOnlyHttpHeaders方法返回可写的HttpHeaders,此时重试正常执行:1次初始请求+2次重试均到达目标API,最终因重试耗尽抛出预期的RuntimeException。

测试报错堆栈:

reactor.core.Exceptions$RetryExhaustedException: Retries exhausted: 2/2
    at reactor.core.Exceptions.retryExhausted(Exceptions.java:308)
    Suppressed: The stacktrace has been enhanced by Reactor, refer to additional information below: 
Error has been observed at the following site(s):
    *__checkpoint ⇢ Request to POST http://localhost:10000/ [DefaultWebClient]
Original Stack Trace:
        at reactor.core.Exceptions.retryExhausted(Exceptions.java:308)
        at reactor.util.retry.RetryBackoffSpec.lambda$static$0(RetryBackoffSpec.java:68)
        at reactor.util.retry.RetryBackoffSpec.lambda$null$4(RetryBackoffSpec.java:
...
Caused by: java.lang.RuntimeException: Not 2xx
    at com.example.config.ApiClientConfiguration.lambda$filter$0(ApiClientConfiguration.java:50)
...

解决方案

问题根源:WebClient的ClientRequest是不可变对象,首次请求后请求头会被包装为ReadOnlyHttpHeaders,重试时复用这个不可变的请求实例,导致编码器尝试修改content-length头时触发异常。

修复代码

修改ApiClientConfiguration中的filter方法,预先将请求头转为可写实例,确保重试时能正常修改头信息:

private ExchangeFilterFunction filter() {
    return (request, next) -> {
        // 复制原请求并构建拥有可写头的新请求
        ClientRequest mutableRequest = ClientRequest.from(request)
                .headers(headers -> {
                    HttpHeaders writableHeaders = new HttpHeaders();
                    writableHeaders.putAll(request.headers());
                    headers.clear();
                    headers.addAll(writableHeaders);
                })
                .build();
        
        return next.exchange(mutableRequest)
                .flatMap(clientResponse -> {
                    if (clientResponse.statusCode().is2xxSuccessful()) {
                        return Mono.just(clientResponse);
                    }
                    return Mono.error(new RuntimeException("Not 2xx"));
                })
                .retryWhen(Retry.fixedDelay(2, Duration.ofSeconds(2)));
    };
}

原理说明

  • ClientRequest本身不可变,首次请求后内部的HttpHeaders会被转为只读状态。
  • 通过ClientRequest.from(request)复制原请求的所有信息,重新创建包含可写HttpHeaders的请求实例。
  • 后续重试时使用这个新的可变请求,编码器可以正常设置content-length等头信息,避免触发UnsupportedOperationException。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 20:00:07