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的方法必须保持原样,避免影响包内其他使用场景。
疑问
- 我的实现哪里有问题?
- 为什么重试时会尝试修改请求头?
- 有没有办法让重试时的请求头变为可写状态?
- 是否忽略了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
相关产品推荐
相关产品推荐

