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

开心一刻

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

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

侄子:叫妈给你

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

侄子:不是,叫妈妈给你

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

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

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

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

需求背景

需求很简单,就是以 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());
    }
}

View Code

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

复制代码
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)

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、多学多总结,多和同事分享沟通,有问题了才好请教他们

相关推荐
装不满的克莱因瓶12 天前
Java高并发异步请求实战,Jmeter暴力压测下的解决方案
jmeter·线程池·多线程·并发·resttemplate·qps·压测
教练、我想打篮球2 个月前
120 同样的 url, header, 参数, 使用 OkHttp 能够成功获取数据, 使用 RestTemplate 报错
http·okhttp·resttemplate·accept
唐僧洗头爱飘柔95275 个月前
【SpringCloud(1)】初识微服务架构:创建一个简单的微服务;java与Spring与微服务;初入RestTemplate
java·spring·spring cloud·微服务·架构·resttemplate·java微服务技术栈
布朗克1686 个月前
Spring Boot项目通过RestTemplate调用三方接口详细教程
java·spring boot·后端·resttemplate
逆风飞翔的小叔1 年前
【微服务】java中http调用组件深入实战详解
okhttp·httpclient·resttemplate·http组件详解·httpclient详解
DanceDonkey1 年前
@LoadBalanced注解的实现原理
rpc·springcloud·resttemplate·客户端负载均衡
王·小白攻城狮·不是那么帅的哥·天文1 年前
springboot配置并使用RestTemplate
java·spring boot·后端·resttemplate
langzitianya1 年前
RestTemplate实时接收Chunked编码传输的HTTP Response
springboot·stream·resttemplate·chunked·流式
CoderJia程序员甲1 年前
重学SpringBoot3-RestTemplate配置与使用详解
java·spring boot·resttemplate·rest
dazhong20122 年前
Springboot 开发之 RestTemplate 简介
java·spring boot·http·resttemplate