一、问题描述

最近笔者在用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=,只不过这类默认的请求头被隐藏起来了,取消隐藏就可以看到,另一个是就算自己申明了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删掉,然后就没有下文了。因此笔者也才会想记录下这个问题,了解下原因,能有个参考,也有所收获。

现在我邀请你进入我们的软件测试学习交流群:746506216】,备注“入群”, 大家可以一起探讨交流软件测试,共同学习软件测试技术、面试等软件测试方方面面,还会有免费直播课,收获更多测试技巧,我们一起进阶Python自动化测试/测试开发,走向高薪之路。

喜欢软件测试的小伙伴们,如果我的博客对你有帮助、如果你喜欢我的博客内容,请 “点赞” “评论” “收藏” 一 键三连哦!