记一次 RestTemplate 请求失败问题的排查 → RestTemplate 默认会对特殊字 ...

宁睿  金牌会员 | 2024-1-13 05:38:54 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 957|帖子 957|积分 2881

开心一刻

  今天中午,侄子在沙发上玩手机,他妹妹屁颠屁颠的跑到他面前
  小侄女:哥哥,给我一块钱
  侄子:叫妈给你
  小侄女朝着侄子,毫不犹豫的叫到:妈!
  侄子:不是,叫妈妈给你
  小侄女继续朝他叫到:妈妈
  侄子受不了,从兜里掏出一块钱说道:我就只有这一块钱了,拿去拿去
  小侄女最后还不忘感谢到:谢谢妈妈!
  侄子彻底奔溃了,我在一旁笑出了鹅叫声

需求背景

  需求很简单,就是以 HTTP 的方式下载 OSS 上的文件,类似如下

  分两步
  1、获取文件的下载地址( HTTP 地址 )
  2、根据下载地址下载文件
  第 1 步不是本文的重点,略过,我们只需要实现第 2 步,是不是很简单?
问题复现

  目前,系统跟其他系统的 HTTP 对接都是用的 RestTemplate 
  那毫无疑问,也用 RestTemplate 来下载 OSS 文件
  测试代码非常简单,如下
  1. package com.qsl;
  2. import org.junit.Test;
  3. import org.junit.runner.RunWith;
  4. import org.springframework.boot.test.context.SpringBootTest;
  5. import org.springframework.http.ResponseEntity;
  6. import org.springframework.test.context.junit4.SpringRunner;
  7. import org.springframework.web.client.RestTemplate;
  8. import javax.annotation.Resource;
  9. /**
  10. * @description: RestTemplate 测试
  11. * @author: 博客园@青石路
  12. * @date: 2023/11/26 15:31
  13. */
  14. @RunWith(SpringRunner.class)
  15. @SpringBootTest
  16. public class RestTemplateTest {
  17.     @Resource
  18.     private RestTemplate restTemplate;
  19.     @Test
  20.     public void testOss() {
  21.         String ossUrl = "https://qsl-yzb-test.oss-cn-wuhan-lr.aliyuncs.com/company_compare_t.sql?Expires=1700987277&OSSAccessKeyId=TMP.3Kf7vKYWL9RHkroENy7hUyrqAhHBC8YpBCnqXAstCyH3K1j6fkZujtL47V1mFkG5e5hmnLD2dVn4ZJGeD2yDh3GAAQc1k8&Signature=O2qiPYvfZyPmeouwzkXcNqC4Oy0%3D";
  22.         ResponseEntity<byte[]> responseEntity = restTemplate.getForEntity(ossUrl, byte[].class);
  23.         System.out.println(responseEntity.getStatusCode());
  24.     }
  25. }
复制代码
View Code
  我们看下执行结果,发现报异常了
  1. org.springframework.web.client.HttpClientErrorException$Forbidden: 403 Forbidden: [<?xml version="1.0" encoding="UTF-8"?>
  2. <Error>
  3.   <Code>AccessDenied</Code>
  4.   <Message>Request has expired.</Message>
  5.   <RequestId>65630E3B05EC713334EDD93D</RequestId>
  6.   <HostId>qsl-yzb-test.oss-cn-wuh... (443 bytes)]
  7.     at org.springframework.web.client.HttpClientErrorException.create(HttpClientErrorException.java:109)
  8.     at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:170)
  9.     at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:112)
  10.     at org.springframework.web.client.ResponseErrorHandler.handleError(ResponseErrorHandler.java:63)
  11.     at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:785)
  12.     at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:743)
  13.     at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:677)
  14.     at org.springframework.web.client.RestTemplate.getForEntity(RestTemplate.java:345)
  15.     at com.qsl.RestTemplateTest.testOss(RestTemplateTest.java:27)
  16.     at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  17.     at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
  18.     at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  19.     at java.lang.reflect.Method.invoke(Method.java:498)
  20.     at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
  21.     at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
  22.     at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
  23.     at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
  24.     at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)
  25.     at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:84)
  26.     at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
  27.     at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
  28.     at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
  29.     at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
  30.     at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
  31.     at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
  32.     at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
  33.     at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
  34.     at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
  35.     at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
  36.     at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
  37.     at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
  38.     at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
  39.     at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
  40.     at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
  41.     at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
  42.     at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
  43.     at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
  44.     at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
  45.     at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
复制代码
View Code
  直接从浏览器下载是正常的,用代码走 RestTemplate 方式下载则失败,提示 403 Forbidden 
  是不是有点懵?

问题排查

  系统中已经用 RestTemplate 对接了很多 HTTP 接口,全部都没问题
  这不就是一个很简单的 HTTP 请求吗,简单的不能再简单了,怎么会失败了?
  直接把我整不会了,不知道从何下手去排查了

  第一时间想到了阿里云 OSS 售后,联系到人工客服,反馈了问题
  客服响应倒是很及时,但却迟迟没有找到问题原因
  然后我又将求助目光转向了部门内同事
  有个同事提到:你开启 debug 日志,看看 RestTemplate 请求地址或参数是不是有什么问题
  我内心其实是拒绝的, HTTP 地址都是现成的,都不用拼接, GET 方式的参数也是直接在 URL 中,能有什么问题?
  但我的手却很诚实,默默的开启了 debug 日志(在配置文件中加上: debug: true )
  执行结果依旧失败,但是多了三行 debug 日志

   RestTemplate 的请求 URL 已经打印出来了,我们来和原始的 URL 对比一下,看看是不是有区别

   不比不知道,一比吓一跳,这特喵的 RestTemplate 是做了手脚呀!对 % 进行了转义处理,处理成 %25 了
  至于为什么需要对 GET 方式的 URL 的特殊字符进行转义,我就不做过多解释了(网上资料很多!),举个例子你们就明白了
     http://localhost:8080/hello?name=青石路 的参数 name 的值是 青石路 ,这个大家都认可吧?
    如果 name 的值是 青石路&路石青 ,这个 URL 应该是怎样的?
    有人可能会有疑问了:你这说的是 &,跟 % 有什么关系?
    你是黑子,来搞我的吧?
    求求你别搞我,我很菜的!

     RFC 3986编码规范 指明了:百分号本身用作对不安全字符进行编码时使用的特殊字符,因此本身需要编码
    例如: %20 表示空格, %2B 表示 +,等等
问题处理

  问题已经找到了,那么该如何处理了?
  抛开上面的问题,处理这种 URL 转义的问题,方式有很多
  1、改成 POST 请求方式
    比较推荐这种方式,奈何这种方式不适用本案例
  2、使用 HttpClient jar
    因为同事用的这种方式实现与本案例一样的下载,没有转义问题
    但为了统一,仍想保留统一的 RestTemplate 方式,即没有采用这种方式
  3、 RestTemplate 的 URI 方式

    本案例最终采用这种的方式

    通过 debug 日志是能够看到, RestTemplate 请求的地址是没有进行转义的(这里不展示了,大家自行去测试!)
    至于 String 和 URI 的差别,大家去 debug 跟下源码就清楚了,底层的实现差别还是很大的哦
  当然还有其他的方式,但是需要结合系统当前的情况,找出最合适的那种方式
总结

  1、别自以为是,该试还得试
  2、 debug 日志是调试的好东西,记得用、用、用!
  3、多学多总结,多和同事分享沟通,有问题了才好请教他们

 

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

宁睿

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表