一、问题描述
最近笔者在用jmeter对一个文件存储服务做压测,由于对jmeter不太熟悉,遇到了一些坑,其中有一个就是用表单上传文件时,一直失败,原因竟是手动加了http请求头:
Content-Type=multipart/form-data
,
二、分析过程
1、问题初现
遇到这个问题,报的错是405,但是查看结果树中,请求的方法就是POST,所以没有从返回的异常中得到什么有用的价值;
2、比对项目中的传参,一模一样
笔者的第一感觉是请求头、或者表单参数传错了,因为原本的jmeter脚本就是对照着项目中的代码写的, 所以将自己之前在自己项目中写的代码跑了一下,看了传参,然而发现一模一样,项目中也能上传成功~这就有点懵了
划重点 ,项目中用的客户端是Spring RestTemplate
具体表单参数由于比较敏感,这里就不贴了
3、使用postman重放一下接口,成功了
用一模一样的参数在postman中重新请求了一遍,发现竟然成功了~
4、抓包比对,发现boundary的存在
由于肉眼上看到的参数都是一模一样的,那只能联想到是RestTemplate、postman自动帮你做了什么事情,所以要看发送出网络请求的实际参数,那就只能抓包来看看了 于是通过tcpdummp分别抓jmeter、postman发起请求时的包
jmeter抓到的请求头
postman抓到的请求头
发现表单参数一模一样,只有请求头有点不太一样,发现postman的请求头,
Content-Type
中多了个boundary;
于是笔者将boundary复制一遍到jmeter的
Content-Type
中,发现还是失败~
于是了解下这个boundary是干嘛用的,经查阅资料,参考RFC规范: datatracker.ietf.org/doc/html/rf… 他的作用大概如下:
当content-type为multipart/form-data类型,数据体中传输了多个参数时,需要用boundary指定分隔符。请求接收端就是通过boundary的值作为分隔符,来解析参数。
5、为什么postman、RestTemplate会有boundary?
postman
实际上在postman中,当我们选择请求body为
form-data
时,postman会默认帮我们生成一个请求头
multipart/form-data; boundary=<calculated when request is sent>
,只不过这类默认的请求头被隐藏起来了,取消隐藏就可以看到,另一个是就算自己申明了
Content-Type=multipart/form-data
,也会被他覆盖,因此可以成功请求。
这里笔者记得看过一篇文章说道,postman也是在某个版本之后,才在当你手动写了请求头
Content-Type=multipart/form-data
,还会自动生成boundary,在此之前手动声明也会报错,但是具体版本不记得了,如果你遇到了可以下载新版本的postman
RestTemplate
笔者通过一路debug,找到了生成boundary的逻辑,笔者在代码中手动加了
Content-Type=multipart/form-data
,但是他会判断Content-Type,如果是multipart/form-data,就会自动帮你生成boundary,并写入到发出的消息中,其关键代码如下:
org.springframework.http.converter.FormHttpMessageConverter#write
方法:
这里首先会判断这个消息体的类型,是否有多部分组成,如果多部分组成,则调用
writeMultipart
方法
public void write(MultiValueMap<String, ?> map, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
if (!isMultipart(map, contentType)) {
writeForm((MultiValueMap<String, String>) map, contentType, outputMessage);
else {
writeMultipart((MultiValueMap<String, Object>) map, outputMessage);
org.springframework.http.converter.FormHttpMessageConverter#writeMultipart
方法
这个方法会先生成一个随机字符串,即boundary,然后将调用writeParts()方法,将所有参数用boundary分隔
private void writeMultipart(final MultiValueMap<String, Object> parts, HttpOutputMessage outputMessage) throws IOException {
// 生成一个随机字符串
final byte[] boundary = generateMultipartBoundary();
Map<String, String> parameters = Collections.singletonMap("boundary", new String(boundary, "US-ASCII"));
MediaType contentType = new MediaType(MediaType.MULTIPART_FORM_DATA, parameters);
HttpHeaders headers = outputMessage.getHeaders();
headers.setContentType(contentType);
// 这个判断条件先不管,不是重点
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
// 将boundary,参数,写入到写出的消息流中
writeParts(outputStream, parts, boundary);
writeEnd(outputStream, boundary);
else {
// 将boundary,参数,写入到写出的消息流中
writeParts(outputMessage.getBody(), parts, boundary);
writeEnd(outputMessage.getBody(), boundary);
org.springframework.http.converter.FormHttpMessageConverter#writeParts
方法
将所有参数,通过boundary分隔,然后写入OutputStream
private void writeParts(OutputStream os, MultiValueMap<String, Object> parts, byte[] boundary) throws IOException {
for (Map.Entry<String, List<Object>> entry : parts.entrySet()) {
String name = entry.getKey();
for (Object part : entry.getValue()) {
if (part != null) {
writeBoundary(os, boundary);
writePart(name, getHttpEntity(part), os);
writeNewLine(os);
综上,这里的流程符合我们前面查阅资料看到的规范
6、那jmeter就不会自动帮你生成boundary?
会生成boundary吗?会
经过查阅资料,实际上jmeter也会自动帮你生成boundary,会失败只是因为手动加了Content-Type=multipart/form-data
,覆盖了默认帮你生成的Content-Type
那怎么搞才可以?
其实去掉手动加的Content-Type,jmeter会默认帮你生成的,经过笔者验证,去掉之后就可以上传成功,抓到的包也符合预期(请求头带有boundary、请求体通过boundary分隔)
那为什么前面手动加上了boundary,还是不行
这是因为你手动加的boundary,覆盖了jmeter自动生成的,而实际上jmeter在处理消息体时,是以他自己生成的boundary分隔的,请求接收端根据你手动写的boundary去做解析,自然会报错。
7、jmeter就不会像postman那样,规避一下手动加入Content-Type的问题吗?
笔者思考到这个问题的时候,看了下自己的jmeter版本,是5.2.1,查了下才发现这个版本在2019年12月的时候发布的,已经两年多了,那这两年多jmeter有没有可能修复了这个问题呢?
去官网看看发布记录:jmeter.apache.org/changes.htm…
没有找到相关的描述
也可能是笔者英语不太好,眼尖的读者可以帮忙找找看。
实际上呢?不甘心的我,还是下载了一个最新版本的5.5,试了一下,真的可以,就算手动加了Content-Type,发出的请求,也会自动帮你加上boundary~
基础很重要
其实这应该是一个http规范的问题,非常基础,如果笔者知道这个知识点,在看到jmeter发出的请求头,没有boundary的时候,就能发现到问题了。
所以说,基础知识可能平时你觉得没用到,但是总会在不经意间帮你解决了很多问题。
分析问题才有所收获
前期笔者在查阅资料的时候,发现很多类似的问题,在使用postman、httpclient、jmeter都遇到了这个问题,但是解决办法都是把Content-Type=multipart/form-data
删掉,然后就没有下文了。因此笔者也才会想记录下这个问题,了解下原因,能有个参考,也有所收获。