PAYATHON 2026

HTTP 401 导致 java.net.HttpRetryException 的原因

数据小许

结论

问题不在 JSON 响应体,而在于 HttpURLConnection 对 401 Unauthorized 的认证处理与请求的 streaming mode 发生了冲突。

收到 401 后,HttpURLConnection 会根据认证挑战尝试重新发送请求。如果请求使用 fixed-length streaming 或 chunked streaming,请求体可能无法安全重放,JDK 就会在 getInputStream() 阶段直接抛出:

java.net.HttpRetryException: cannot retry due to server authentication, in streaming mode

400、402`` 等状态码不会触发 HTTP 认证重试,因此可以正常读取响应体。自定义 ResponseErrorHandler只能改变 Spring 判断状态码的方式,无法关闭HttpURLConnection` 内部的认证处理。

为什么以前正常、现在出现异常

这通常说明客户端的请求方式或认证链路发生了变化,例如:

  • SimpleClientHttpRequestFactory 开始使用 fixed-length 或 chunked streaming 发送请求体。
  • 请求从没有 body 的 GET 变成了带 body 的 POST 或 PUT。
  • Spring、JDK 或 HTTP request factory 被升级或替换。
  • 原先会提前发送的 Authorization 请求头不再发送,导致服务端先返回 401。
  • Spring Security、Servlet Filter、反向代理或 WebSphere 安全配置开始介入认证。
  • 服务端原先返回普通业务错误,现在改为返回 401。

Fiddler 中两个响应是否包含 Connection: Close,通常不是问题所在。关键区别在于,401 对 HttpURLConnection 属于认证协议的一部分,而 402 只是普通错误响应。

优先解决方案

1. 不要用 401 表示普通业务错误

如果“找不到用户”表示业务数据不存在,而不是请求缺少有效的 HTTP 身份认证,可以考虑返回:

HTTP/1.1 404 Not Found
Content-Type: application/json

也可以根据接口语义使用 400、403 或 422 等状态码。

401 Unauthorized 的准确含义是请求缺少有效的身份认证信息。规范的 401 响应还应包含 WWW-Authenticate,例如:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="api"
Content-Type: application/json

{"error":"Authentication is required."}

示例响应中没有 WWW-Authenticate。如果这里确实发生了认证失败,需要检查安全过滤器、代理或应用服务器是否生成了不完整的 401 响应。

2. 提前发送认证信息,避免先收到 401

如果接口确实需要 Basic Authentication,可以让客户端在第一次请求时就发送凭据:

RestTemplate restTemplate = new RestTemplate();

restTemplate.getInterceptors().add((request, body, execution) -> {
    request.getHeaders().setBasicAuth(username, password);
    return execution.execute(request, body);
});

这样服务端不会先返回认证挑战,HttpURLConnection 也不必重新发送带请求体的请求。

Basic Authentication 必须通过 HTTPS 使用,否则凭据只是经过 Base64 编码,并没有加密。

3. 对旧版 Spring 关闭 HttpURLConnection 的输出流模式

部分旧版 Spring 支持配置 SimpleClientHttpRequestFactory:

SimpleClientHttpRequestFactory requestFactory =
        new SimpleClientHttpRequestFactory();

requestFactory.setOutputStreaming(false);

RestTemplate restTemplate = new RestTemplate(requestFactory);

关闭输出流模式后,HttpURLConnection 会先在内存中缓冲请求体,因此能够进行认证重试。

需要注意:

  • setOutputStreaming(false) 会增加内存占用,不适合大型上传。
  • 该 API 在不同 Spring 版本中的可用性和行为并不相同。较新的 Spring 版本可能已经弃用或移除了相关配置。
  • 只设置 setBufferRequestBody(true) 不一定有效,因为底层仍有可能调用 setFixedLengthStreamingMode()。

4. 换用其他 HTTP Client

如果需要稳定处理认证挑战、重定向和错误响应体,可以把 RestTemplate 的底层实现换成 Apache HttpClient:

HttpComponentsClientHttpRequestFactory requestFactory =
        new HttpComponentsClientHttpRequestFactory();

RestTemplate restTemplate = new RestTemplate(requestFactory);

具体依赖和认证配置取决于当前使用的 Spring 与 Apache HttpClient 主版本,HttpClient 4 和 HttpClient 5 的配置代码不能直接混用。

更换 request factory 可以绕开 HttpURLConnection 的这项限制。不过,只要接口涉及认证,仍然需要正确配置凭据和认证方案。

自定义 ErrorHandler 为什么解决不了

下面这类处理只能告诉 RestTemplate:“不要把这个状态码视为需要抛出 Spring 异常的错误。”

@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
    return false;
}

但在实际调用过程中,底层连接仍需先取得响应流。HttpURLConnection.getInputStream() 可能在暴露响应体之前进入 401 认证重试分支,并抛出 HttpRetryException。所以,即使 Spring 已经准备正常反序列化 JSON,也无法取得输入流。

直接改用 getErrorStream() 有时可以读到错误响应,但结果取决于 JDK 实现和连接状态,不能作为可靠的认证处理方案。

排查重点

建议对照代码变更前后的以下内容:

  1. RestTemplate 使用的 ClientHttpRequestFactory 是否发生变化。
  2. 是否启用了 fixed-length 或 chunked streaming。
  3. 请求是否新增了 body,或者 body 大小是否明显变化。
  4. Authorization 请求头是否仍会随第一次请求发送。
  5. Spring Security Filter、Servlet Filter、代理或 WebSphere 安全配置是否开始处理该 URL。
  6. 401 是由业务代码生成,还是由认证组件覆盖或再次生成。
  7. 响应是否包含正确的 WWW-Authenticate 请求头。

最稳妥的做法是先纠正状态码语义。普通业务错误不应返回 401。如果确实是认证失败,服务端应正确发送认证挑战,客户端则应提前携带凭据,或改用支持认证重试的 HTTP Client。

备注:内容仅供参考。