记一次RestTemplate的问题
记一次 RestTemplate 调用接口返回 401 : [no body] 问题复盘
一、问题现象
SP Demo 应用通过 OAuth2 授权码流程完成登录后,在回调接口 /sp-demo/oauth/callback 中使用 RestTemplate 调用本地 /oauth2/token 端点换取 Token。
异常表现:
- RestTemplate 抛出
HttpClientErrorException,message 为401 : [no body] - 全局异常处理器
GlobalExceptionHandler.handleOAuth2()断点确认已经执行 - 服务端确实写入了 JSON 响应体
{"error":"invalid_client","error_description":"客户端认证失败"} - 但 RestTemplate 侧始终拿不到 body
二、排查过程
2.1 初始怀疑:OAuth2Exception 未被全局异常处理器捕获
- 检查发现
OAuth2Service.processToken()中client.getClientSecret().equals(clientSecret)存在 NPE 风险 - 当
client.getClientSecret()为 null 时会抛NullPointerException而非OAuth2Exception - 修复:改为
!Objects.equals(clientSecret, client.getClientSecret())
但修复后问题依旧。
2.2 尝试增强服务端响应写入
依次尝试了以下方案,均无效:
| 尝试 | 方案 | 结果 |
|---|---|---|
| 1 | @ControllerAdvice → @RestControllerAdvice |
无效 |
| 2 | ResponseEntity 显式设置 .contentType(MediaType.APPLICATION_JSON) |
无效 |
| 3 | 直接写 HttpServletResponse:response.getWriter().write(json) + flush() |
无效 |
| 4 | application.yml 添加 server.error.include-message: always |
无效 |
2.3 最终定位:JDK HttpURLConnection 的 errorStream 问题
RestTemplate 默认使用 SimpleClientHttpRequestFactory,底层是 JDK 的 HttpURLConnection。
关键机制:
1 | HttpURLConnection 对 HTTP 响应的处理: |
而 RestTemplate 的 DefaultResponseErrorHandler 读取 body 的逻辑:
- 判断响应状态为 4xx → 标记为错误
- 尝试从
response.getBody()读取(本质是inputStream) - 此时
inputStream为空(内容在errorStream中) - 最终得到
[no body]
结论:body 没有被服务端”吞掉”,而是 RestTemplate 底层读错了流。
三、解决方案
最终方案:替换为 Apache HttpClient
Apache HttpClient 不区分 inputStream / errorStream,统一从连接中读取响应体:
1 | try (CloseableHttpClient httpClient = HttpClients.createDefault()) { |
备选方案对比
| 方案 | 可行性 | 说明 |
|---|---|---|
| Apache HttpClient(采用) | ✅ 最佳 | 行为可控,不依赖 Spring 框架行为 |
| RestTemplate + HttpComponentsClientHttpRequestFactory | ✅ 可行 | 让 RestTemplate 底层也用 Apache HttpClient |
| 自定义 ResponseErrorHandler 手动读 errorStream | ⚠️ 脆弱 | 需要反射或 hack,不推荐 |
| OkHttp | ✅ 可行 | 同样不区分 error/input stream,但需额外依赖 |
四、经验总结
- RestTemplate + JDK HttpURLConnection 在 4xx/5xx 时会将信息存入errorStream或丢失响应体
- 在需要精确控制 HTTP 请求/响应的场景下,优先使用 Apache HttpClient 或 OkHttp,避免被 Spring 框架的抽象层屏蔽底层细节
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 KewenBlogs!
评论
