开心一刻

  今天中午,侄子在沙发上玩手机,他妹妹屁颠屁颠的跑到他面前

  小侄女:哥哥,给我一块钱

  侄子:叫妈给你

  小侄女朝着侄子,毫不犹豫的叫到:妈!

  侄子:不是,叫妈妈给你

  小侄女继续朝他叫到:妈妈

  侄子受不了,从兜里掏出一块钱说道:我就只有这一块钱了,拿去拿去

  小侄女最后还不忘感谢到:谢谢妈妈!

  侄子彻底奔溃了,我在一旁笑出了鹅叫声

需求背景

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

  分两步

  1、获取文件的下载地址( HTTP 地址 )

  2、根据下载地址下载文件

  第 1 步不是本文的重点,略过,我们只需要实现第 2 步,是不是很简单?

问题复现

  目前,系统跟其他系统的 HTTP 对接都是用的 RestTemplate

  那毫无疑问,也用 RestTemplate 来下载 OSS 文件

  测试代码非常简单,如下

package com.qsl;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; /**
* @description: RestTemplate 测试
* @author: 博客园@青石路
* @date: 2023/11/26 15:31
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class RestTemplateTest { @Resource
private RestTemplate restTemplate; @Test
public void testOss() {
String ossUrl = "https://qsl-yzb-test.oss-cn-wuhan-lr.aliyuncs.com/company_compare_t.sql?Expires=1700987277&OSSAccessKeyId=TMP.3Kf7vKYWL9RHkroENy7hUyrqAhHBC8YpBCnqXAstCyH3K1j6fkZujtL47V1mFkG5e5hmnLD2dVn4ZJGeD2yDh3GAAQc1k8&Signature=O2qiPYvfZyPmeouwzkXcNqC4Oy0%3D";
ResponseEntity<byte[]> responseEntity = restTemplate.getForEntity(ossUrl, byte[].class);
System.out.println(responseEntity.getStatusCode());
}
}

  我们看下执行结果,发现报异常了

org.springframework.web.client.HttpClientErrorException$Forbidden: 403 Forbidden: [<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>AccessDenied</Code>
<Message>Request has expired.</Message>
<RequestId>65630E3B05EC713334EDD93D</RequestId>
<HostId>qsl-yzb-test.oss-cn-wuh... (443 bytes)] at org.springframework.web.client.HttpClientErrorException.create(HttpClientErrorException.java:109)
at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:170)
at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:112)
at org.springframework.web.client.ResponseErrorHandler.handleError(ResponseErrorHandler.java:63)
at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:785)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:743)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:677)
at org.springframework.web.client.RestTemplate.getForEntity(RestTemplate.java:345)
at com.qsl.RestTemplateTest.testOss(RestTemplateTest.java:27)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)
at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:84)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)

  直接从浏览器下载是正常的,用代码走 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、多学多总结,多和同事分享沟通,有问题了才好请教他们

记一次 RestTemplate 请求失败问题的排查 → RestTemplate 默认会对特殊字符进行转义的更多相关文章

  1. Resttemplate请求失败如何获取返回的json

    参考:https://blog.csdn.net/u011974797/article/details/82424004 https://www.cnblogs.com/liumz0323/p/106 ...

  2. 记一次因证书问题导致请求失败问题SSLHandshakeException

    记一次因证书问题导致请求失败问题SSLHandshakeException 转载请注明出处:https://www.cnblogs.com/funnyzpc/p/10989813.html 最近接一外 ...

  3. 精讲RestTemplate第8篇-请求失败自动重试机制

    本文是精讲RestTemplate第8篇,前篇的blog访问地址如下: 精讲RestTemplate第1篇-在Spring或非Spring环境下如何使用 精讲RestTemplate第2篇-多种底层H ...

  4. 精讲RestTemplate第7篇-自定义请求失败异常处理

    本文是精讲RestTemplate第7篇,前篇的blog访问地址如下: 精讲RestTemplate第1篇-在Spring或非Spring环境下如何使用 精讲RestTemplate第2篇-多种底层H ...

  5. Jsoup问题---获取http协议请求失败 org.jsoup.UnsupportedMimeTypeException: Unhandled content type. Must be text/*, application/xml, or application/xhtml+xml.

    Jsoup问题---获取http协议请求失败 1.问题:用Jsoup在获取一些网站的数据时,起初获取很顺利,但是在访问某浪的数据是Jsoup报错,应该是请求头里面的请求类型(ContextType)不 ...

  6. [django]windows下用Django,静态文件请求失败,出现UnicodeDecodeError

    问题:windows下用Django,静态文件请求失败,出现UnicodeDecodeError:'utf-8' codec can't decode byte 0xb0 in position 1: ...

  7. SQL 2008 R2 启动失败 提示 请求失败或服务未及时响应

    为什么启动sql server 配置管理器出现请求失败或服务未及时响应_百度知道 http://zhidao.baidu.com/link?url=ElemzIan6I2CqJsd7-7uk5TV25 ...

  8. WebApi(四)-Post接口请求失败或接受不到参数(解决方法)

    post方式只能接受一个参数而且必须用FromBody特性标识,所以当没有使用FromBody特性标识的时候就会请求失败,如有添加添加了那访问接口时候参数应传对象不能是key:val的格式否则会接收到 ...

  9. RestTemplate发送请求并携带header信息 RestTemplate post json格式带header信息

    原文地址:  http://www.cnblogs.com/hujunzheng/p/6018505.html RestTemplate发送请求并携带header信息   v1.使用restTempl ...

  10. Ajax请求参数较长导致请求失败

    Ajax请求参数比较长,第5行参数大概1100个字符吧,是接口的请求报文. $.ajax({ type:"POST", url:"${ctx}/test.action?m ...

随机推荐

  1. linux vim 无权限保存解决办法

    通常在vim编辑文件时往往会忘记文件权限问题, 在wq保存时发现权限不足,这时候输入以下命令解决: w! sudo tee % 命令解析: w! {cmd} 指示 保存时执行额外命令: tee 用于将 ...

  2. 开源元数据管理平台Datahub最新版本0.10.5——安装部署手册(附离线安装包)

    大家好,我是独孤风. 开源元数据管理平台Datahub近期得到了飞速的发展.已经更新到了0.10.5的版本,来咨询我的小伙伴也越来越多,特别是安装过程有很多问题.本文经过和群里大伙伴的共同讨论,总结出 ...

  3. Canvas好难,如何让研发低成本实现Web端流程图设计功能

    摘要:本文由葡萄城技术团队于博客园原创并首发.转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 前言 相信大家在职场中经常会用到流程图,在互联网行业,绘制流程 ...

  4. 通过python,将excel中的数据写入二维列表

    需求:读取Excel表中数据,每行数据放在一个列表中,再把所有列表都存入到一个列表中,形成二维列表. 实现方法:导入可在Python处理Excel表格数据的模块. excel表: 方法一:xlwing ...

  5. c# 如何将程序加密隐藏?

    下面将介绍如何通过LiteDB将自己的程序进行加密,首先介绍一下LiteDB. LiteDB LiteDB是一个轻量级的嵌入式数据库,它是用C#编写的,适用于.NET平台.它的设计目标是提供一个简单易 ...

  6. Mysql高级8-触发器

    一.触发器 触发器是与表有关的数据库对象,指在insert/update/delete之前或者之后,触发并执行触发器中定义的sql语句集合,触发器的这种特性可以协助应用在数据库端确保数据的完整性,日志 ...

  7. ChatGPT应用篇:如何快速生成精美PPT提高工作效率-附资料下载

    一.ChatGPT生成markdown源代码 问: 我想做一份ChatGPT变现方法的PPT,请生成丰富的教学展示内容,因为生成PPT是需要MarkDown格式的,请您输出Markdown格式的内容 ...

  8. 2.7 PE结构:重定位表详细解析

    重定位表(Relocation Table)是Windows PE可执行文件中的一部分,主要记录了与地址相关的信息,它在程序加载和运行时被用来修改程序代码中的地址的值,因为程序在不同的内存地址中加载时 ...

  9. Skynet:Debug Console的扩展

    起因 最近上线服务器遇到了一些问题,上个月CPU暴涨的问题,那个经查验是死循环导致endless loop了. 这周又遇到了mem占用达到96%的问题,在debug console里调用了gc之后,跌 ...

  10. 从MVC到DDD,该如何下手重构?

    作者:付政委 博客:bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 大家好,我是技术UP主小傅哥.多年的 DDD 应用,使我开了技术的眼界! MVC 旧工程腐化严重,迭代成本太高 ...