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

Buffering包装下Jdk与SimpleClientHttpRequestFactory遇IOException的行为差异

Spring RestClient 两种请求工厂读取4XX响应体的行为差异成因

在配置Spring RestClient时,为方便开发阶段记录响应,用BufferingClientHttpRequestFactory包装了请求工厂。但调用返回4XX错误状态码的接口时,JdkClientHttpRequestFactory能正常一次读取响应体,而SimpleClientHttpRequestFactory需要两次调用clientHttpResponse.getBody()(先捕获IOException再读取)才能拿到响应体,以下是该差异的成因解析。

配置代码

@Bean
public RestClient restClient() {
    // 在SimpleClientHttpRequestFactory和JdkClientHttpRequestFactory间切换
    final var clientHttpRequestFactory = new SimpleClientHttpRequestFactory();

    return RestClient.builder()
        .requestFactory(new BufferingClientHttpRequestFactory(clientHttpRequestFactory))
        .requestInterceptors(list -> list.add(new LoggingClientHttpRequestInterceptor()))
        .build();
}

日志拦截器代码

@Slf4j
private static class LoggingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {

    @Override
    public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
        // 执行REST调用
        final var clientHttpResponse = execution.execute(request, body);
        // 复制并记录响应体
        try {
            // 第一次读取InputStream
            log.info("#1 attempt");
            final var response = StreamUtils.copyToString(clientHttpResponse.getBody(), UTF_8);
            log.info("#1 status={}, response={}", clientHttpResponse.getStatusCode().value(), response);
        } catch (IOException e) {
            // 第二次读取InputStream
            log.info("#2 attempt");
            final var response = StreamUtils.copyToString(clientHttpResponse.getBody(), UTF_8);
            log.info("#2 status={}, response={}", clientHttpResponse.getStatusCode().value(), response);
        }

        // 返回客户端响应
        return clientHttpResponse;
   }
}

两种工厂的日志输出对比

  • JdkClientHttpRequestFactory(预期行为):
2024-01-04T15:25:41.138+07:00  INFO 62777 --- [main] : #1 attempt
2024-01-04T15:25:41.140+07:00  INFO 62777 --- [main] : #1 status=400, response={"foo": "bar"}
2024-01-04T15:25:41.141+07:00  INFO 62777 --- [main] : Done
  • SimpleClientHttpRequestFactory(意外行为):
2024-01-04T15:26:41.518+07:00  INFO 62839 --- [main] : #1 attempt
2024-01-04T15:26:41.767+07:00  INFO 62839 --- [main] : #2 attempt
2024-01-04T15:26:41.769+07:00  INFO 62839 --- [main] : #2 status=400, response={"foo": "bar"}
2024-01-04T15:26:41.771+07:00  INFO 62839 --- [main] : Done

行为差异成因

  1. SimpleClientHttpRequestFactory的底层逻辑
    它基于JDK的HttpURLConnection实现。当响应状态码为4XX时,HttpURLConnection默认将响应内容存入错误流(error stream),而非普通输入流。第一次调用getBody()时,它尝试读取普通输入流,此时流为空,直接抛出IOException;第二次调用时,底层会自动切换到读取错误流,从而成功获取响应体。

  2. JdkClientHttpRequestFactory的处理方式
    它基于JDK 11+的HttpClient实现,该客户端不区分普通输入流和错误流——无论状态码是否为4XX,响应内容都通过同一个输入流返回,因此第一次调用getBody()就能正常读取到内容。

  3. BufferingClientHttpRequestFactory的作用限制
    它仅负责缓存响应体,不会修改底层请求工厂对错误流的处理逻辑,因此两种工厂的原生行为差异依然会暴露出来。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 22:57:38