RestTemplate 调用报错:400 BAD_REQUEST
3079字约10分钟
2024-08-08
背景
需求中涉及视频文件上传,上传文件接口采用的是 POST
请求方式,像视频名称、类型等额外的参数就直接拼接在 URL
上的(url?name=xxx&type=xxx
)。在上传接口中还需要使用 RestTemplate
调用其他服务查询数据
在测试过程中,发现上传视频的时候有时成功有时失败,随机一步一步复现排查问题
排查方向
以前使用 RestTemplate
调用其他内部或外部服务时,也遇到过一些调用失败的情况,由于 http
请求使用的 Spring RestTemplate
是做了自定义配置的,所以一直觉得问题是出在请求其他服务时携带了一些莫名其妙的 header
信息所导致的,之前都直接采用 new RestTemplate()
的方式解决了问题
1、文件过大上传失败
测试反馈说上传的视频文件比较大时,上传会失败。在拿到测试用的视频后进行问题复现,当时是百分百的上传失败。问题好复现,接下来开始查看服务日志,发现上传接口在调用其他服务时出现了响应状态码异常(400 BAD_REQUEST
) 。400
这个错误,都是一些参数传递错误、请求语法问题,日志中打印的请求内容语法是正确的,也没管那么多,直接采用 new RestTemplate()
的方式处理了
2、小文件上传也会失败
上传接口中调用了两个内部服务,修改的时候只修改了其中一个,在系统升级之后,测试依旧反馈上传失败,查看服务日志发现未修改的服务调用还是报 400 BAD_REQUEST
这个错误,那我只要把另外个服务调用的 RestTemplate
修改下就能解决问题了,这时候测试反馈说,用小文件上传也会失败,这时候就觉得有点莫名其妙了,但是不理会这个问题,因为只要修改了 RestTemplate
所有的问题都会迎刃而解
3、上传时填写的视频名称有影响
在后续测试过程中发现,上传时填写的视频名称较短时不容易报错,继而发现,如果视频名称填的是纯数字,那么无论是小文件、大文件上传都能一次成功。到了这里排查问题有了明确的方向,那就是视频名称是中文有问题
正式排查问题
复现问题
1、修改项目自定义的
RestTemplate
配置,增加请求时header
内容日志打印2、启动服务,手动调用上传接口
3、观察日志发现在请求其他服务时,
header
中还携带了param_type
、param_title
等内容,title
就是上传是填写的视频名称
- ======= request begin ========
- request uri : http://127.0.0.1:9999/user?id=1
- method : GET
- headers : [Accept:"text/plain, application/xml, text/xml, application/json, application/*+xml, application/*+json, */*", "application/json", Content-Length:"0",
param_type:"1",
param_title:"发降但是佛挡杀佛是是是额的大幅度什么呀对方水电费水电费的呀水电费水电费什么呀对方水电费水电费三大发送到发大水发", postman-token:"c554833f-f3c2-462a-9bcb-064b88b9a702", host:"127.0.0.1:9501", X-B3-TraceId:"6498026c214f0558", connection:"keep-alive", content-type:"multipart/form-data; boundary=--------------------------758843970652402181615447", accept-encoding:"gzip, deflate, br", user-agent:"PostmanRuntime/7.28.4", sessionid:"6748b2755020446199d3448dd884e97f"]
- request body :
- ======== request end =========
- Response Status code : 400 BAD_REQUEST
- 服务查询异常:{}
到这里,问题已经可以复现了,接下来只要看一看为什么报错就能解决问题了
进一步探究
通过前面请求时打印的 header
日志,问题大概率是 header
中携带的参数引起的,其中中文字符的嫌疑特别大,接下来就具体看一下被调用服务所接收的 header
值
1、修改被调用服务,增加请求
header
参数内容打印2、调用上传接口
我们先来看看请求失败的时候,被调用服务所产生的日志。does not conform to RFC 7230 and has been ignored(不符合 RFC 7230,已被忽略)
,也就是说这次请求是不合法的,Tomcat 9.0
中对未编码的内容进行了严格限制。
参考:https://stackoverflow.com/questions/58593645/tomcat-9-header-line-does-not-conform-to-rfc-7230
2023-06-25 17:01:34.994 INFO 112464 --- [nio-9999-exec-1] o.apache.coyote.http11.Http11Processor : Error parsing HTTP request header
Note: further occurrences of HTTP request parsing errors will be logged at DEBUG level.
java.lang.IllegalArgumentException: The HTTP header line [param_title:0xd1MF/[!@[///0x9d0x84'E0xa60xc0H@0xf90xb94594590x84@4594590xc0H@0xf90xb94594590x09'0xd10xd10x0100xd1'40xd1] does not conform to RFC 7230 and has been ignored.
at org.apache.coyote.http11.Http11InputBuffer.skipLine(Http11InputBuffer.java:1059) ~[tomcat-embed-core-9.0.50.jar:9.0.50]
at org.apache.coyote.http11.Http11InputBuffer.parseHeader(Http11InputBuffer.java:980) ~[tomcat-embed-core-9.0.50.jar:9.0.50]
at org.apache.coyote.http11.Http11InputBuffer.parseHeaders(Http11InputBuffer.java:604) ~[tomcat-embed-core-9.0.50.jar:9.0.50]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:292) ~[tomcat-embed-core-9.0.50.jar:9.0.50]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) [tomcat-embed-core-9.0.50.jar:9.0.50]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:893) [tomcat-embed-core-9.0.50.jar:9.0.50]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1723) [tomcat-embed-core-9.0.50.jar:9.0.50]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.50.jar:9.0.50]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [na:1.8.0_92]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [na:1.8.0_92]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.50.jar:9.0.50]
at java.lang.Thread.run(Thread.java:745) [na:1.8.0_92]
我们再来看看请求成功的时候,被调用服务所产生的日志,同样是中文,但是因为乱码之后的内容不涉及到 Tomcat
中的未编码内容限制,是可以请求成功的
: Initializing Spring DispatcherServlet 'dispatcherServlet'
: Initializing Servlet 'dispatcherServlet'
: Completed initialization in 2 ms
: accept ==> text/plain, application/xml, text/xml, application/json, application/*+xml, application/*+json, */*
: authorization ==> Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxNTcxMDU2MzcwOSIsImNyZWF0ZWQiOjE2ODczMzQwMjE2MDksImV4cCI6MTY4NzY5NDAyMX0.GSSs0RJCQzaTSOOC76MLhM1lpmExefoaAOTTTrgbbCLvGbveY6IvxgvX7o_6UfT2FPbZ-AJ0NYy-RxO_BJY6tQ
: param_type ==> 1
: param_title ==> ÑMF/[!@[///'E¦ÀH@ù¹45945
: postman-token ==> 5d75a285-6e37-4b1f-8e9d-8961ed7834c4
: host ==> 127.0.0.1:9501
: x-b3-traceid ==> 6498048a20f89518
: connection ==> keep-alive
: content-type ==> multipart/form-data; boundary=--------------------------096539922886785369892853
: accept-encoding ==> gzip, deflate, br
: user-agent ==> PostmanRuntime/7.28.4
: sessionid ==> 0e9f2b1118d344e6ab8a56f1e201618c
查了下项目使用的 Spring Boot
内置 Tomcat
容器版本,如果说是 9.0
以前的版本,没有这么严格的限制,应该是可以请求成功的
再进一步探究
问题找到了,那我们进行一波验证,在 RestTemplate
发起请求前,将 header
中的中文内容进行 urlEncode
,经过测试,之前上传有问题的视频名称也能成功调用其他服务
@Slf4j
public class TrackLogClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
private void trackRequest(HttpRequest request, byte[] body)
throws UnsupportedEncodingException {
log.debug("======= request begin ========");
log.info("request uri : {}", request.getURI());
HttpHeaders headers = request.getHeaders();
headers.add("sessionid", MDC.get("sessionid"));
headers.add("requesturi", MDC.get("requesturi"));
// 直接从 header 中取出 param_title 参数值,进行 urlEnCode 编码
headers.set("param_title", URLEncoder.encode(headers.get("param_title").toString(), StandardCharsets.UTF_8.name()));
log.debug("method : {}", request.getMethod());
log.debug("headers : {}", request.getHeaders());
log.info("request body : {}", new String(body, "UTF-8"));
log.debug("======== request end =========");
}
}
再再进一步探究
文件上传失败的原因是找到了,那再思考下为什么使用项目配置的 RestTemplate
会携带上文件上传接口的 header
以及 url
拼接的参数呢。通过查找项目中关于 http
请求的拦截器,找到了一个对 RestTemplate
请求进行逻辑处理的拦截器,如下,一个练习时长两年半的 bug
,这个拦截器处理会将接口请求中的参数放入到 RestTemplate
调用其他服务时的 header
中
/**
* @Description 单点登录Header增加
* @Date 2020/8/3 22:02
* @Version 1.0
**/
@Slf4j
public class SSOHeadClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes,
ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
HttpHeaders headers = httpRequest.getHeaders();
InvocationContext context = ContextUtils.getInvocationContext();
// 判断 invocationContext 是否为空
if (context != null) {
Map<String, String> params = context.getContext();
params.forEach((key, value) -> {
// 如果 header 中无该 key 的话,再执行添加
if(!headers.containsKey(key)){
headers.add(key, value);
}
});
}
headers.add("Accept", "application/json");
ClientHttpResponse response = clientHttpRequestExecution.execute(httpRequest, bytes);
return response;
}
}
断点调试内容可以看到,像拼接在 url
上的参数,则也会被添加到 RestTemplate
请求的 header
中,前面加上 param_
Spring Boot
常用拦截器(HandlerInterceptor
、ClientHttpRequestInterceptor
、RequestInterceptor
),这 3
个拦截器的共同点,都是对 http
请求进行拦截,但是 http
请求的来源不同
HandlerInterceptor
是最常规的,其拦截的http
请求是来自于客户端浏览器之类的,是最常见的http
请求拦截器ClientHttpRequestInterceptor
是对RestTemplate
的请求进行拦截的,在项目中直接使用restTemplate.getForObject
的时候,会对这种请求进行拦截,经常被称为:RestTempalte
拦截器或者Ribbon
拦截器RequestInterceptor
常被称为是Feign
拦截器,由于Feign
调用底层实际上还是http
调用,因此也是一个http
拦截器,在项目中使用Feign
调用的时候,可以使用此拦截器
解决方案
1、不管他,涉及中文拼接在
url
上的接口采用不使用项目配置的RestTemplate
2、改造拦截器,对于
header
中单点登录参数进行key
值判断再添加,此改造需要进行回归测试
思考问题
为何最初复现问题时,大文件 + 中文短字符 也是百分百复现?
在本地(Windows)直接接口测试时,大文件 + 中文短字符 没有一点问题。在测试环境(CentOS)时可能是系统不一样,中文乱码不一样
回首往事
调用外部接口时请求异常
在与外部平台对接中,本地、测试、灰度环境使用项目中配置的 RestTemplate
调用接口都没有问题,等到升级到生产后,访问接口出现问题,经过长时间协调对端抓包排查,发现 header
中的 content-type
值有问题,虽然当时问题解决了,但是问题原因现在才知道。使用项目配置的 RestTemplate
会把页面请求时的 applicaiton/json
添加到 header
中,当时在代码中又手动设置 application/json
,导致 header
中的 content-type
有问题
我与文件上传另外一件不为人知的事
采用了项目配置的 RestTemplate
发送文件上传请求,则会出现 the request was rejected because no multipart boundary was found
异常,如下图所示
看异常信息提示:本次请求拒绝是因为没有 multipart boundary
,那这个 boundary
是什么呢?我们在平时浏览器上上传以及在 Postman
中进行文件上传测试,也从来没有设置过 boundary
呀,那很大可能是浏览器、Postman
中上传文件时候自动给我带上的,那这样猜测的话,那 RestTemplate
那也应该会给我们自动加上才对
带着我们的猜测,我们需要做下面两件事:
1、找一个文件上传接口,在浏览器上,看一下有没有
boundary
这个东西2、找一个文件上传接口,使用
Postman
进行文件上传,看一下有没有boundary
这个东西
我们先拿 Postman
试试,结果发现他的 Content-Type
并没有 boundary
这个东东,有点失望,那再看看浏览器的再说吧
在浏览器执行上传请求,很幸运,我们看到了 boundary
,也就是说,我们使用的 RestTemplate
并没有为我们携带上 boundary
这个东东。按道理来说, Spring
都提供了这个功能,那肯定不可能少你一个 boundary
吧,那 RestTemplate
没有给我们加上 boundary
,是否是我们的全局配置引起的问题呢
浏览器和 Postman
调用上传时都能上传成功,那 Postman
也应该会带上 boundary
才对,我们去代码中打印请求的 headers
瞧一瞧,果不其然,在请求的接口中打印出来,是有 boundary
这个东东,而且每一次请求都不一样
事情到这里已经变得比较清晰了,那我们直接手动创建一个 RestTemplate
对象去进行文件上传请求,这一次上传成功了。在全局配置中,我们只配置拦截器和 HttpMessageConverter
,首先拦截器肯定是没有问题的,并不影响 RestTemplate
为我们加上 boundary
,那就是因为我们配置的 FastJsonHttpMessageConverter
造成的
这里提供两种解决方式,一种就是文件上传的就每次手动 new
一个 RestTemplate
,另外一种就是,再写一个配置,不用替换掉原有的 HttpMessageConverter
/*
* @ClassName RestConfig
* @Date 2020/5/13 15:56
* @Version 1.0
**/
@Configuration
public class RestConfig {
@Bean
public static RestTemplate mmsRestTemplate(){
RestTemplate restTemplate = RestTemplateBuilder.create();
//配置自定义的interceptor拦截器
List<ClientHttpRequestInterceptor> interceptors=new ArrayList<ClientHttpRequestInterceptor>();
interceptors.add(new HeadClientHttpRequestInterceptor());
interceptors.add(new TrackLogClientHttpRequestInterceptor());
// RestTemplate 中对字符串使用的是 StringHttpMessageConverter 中默认的编码
// 此编码为(ISO_8859_1)会引起中文乱码,故改用 FastJsonHttpMessageConverter
restTemplate.getMessageConverters().clear();
restTemplate.getMessageConverters().add(new FastJsonHttpMessageConverter());
return restTemplate;
}
}