系统从Spring5升级到Spring6, 除了要进行把javax的api迁移到Jakarta、升级Servlet容器到支持你所选的Jakarta的版本、升级Spring Security对应的API等这些常规操作,还可能遇到一些trick的问题。这里聊一下因为老系统没有限制客户端发送 multipart/related 这种请求而在升级后造成的问题及解决,虽然感觉这种场景 99.999% 的项目都不会遇到。
以流水帐的方式过一下。
升级前Spring5+Jetty9, 升级后Spring6+Jetty11
1)系统上线几天后,有客户说upload csv文件不成功。
2)最后从SumoLogic日志中发现原因是ContentType格式不对,又进一步确认是Spring5的系统支持 multipart/related 而Spring6 不支持造成的。
Caused by: jakarta.servlet.ServletException: Unsupported Content-Type [Multipart/Related; boundary=AAABBB; type="text/xml"; start="root-part--123"], expected [multipart/form-data]
at org.eclipse.jetty.server.Request.getParts(Request.java:2324) ~[jetty-server-11.0.19.jar!/:11.0.19]
3)Spring5 是使用Spring自带的MultipartParser,在解析后传给 servlet controller。系统使用Jetty9作为servlet容器。
4)Spring6 之后之前的 CommonsMultipartResolver 被替换为 StandardServletMultipartResolver。而StandardServletMultipartResolver会依赖容器来对Multipart请求做解析。(容器的实现必然有差别)
Several outdated Servlet-based integrations have been dropped: e.g. Apache Commons FileUpload (org.springframework.web.multipart.commons.CommonsMultipartResolver), and Apache Tiles as well as FreeMarker JSP support in the corresponding org.springframework.web.servlet.view subpackages. We recommend org.springframework.web.multipart.support.StandardServletMultipartResolver
5)其实不管Jetty 9还是Jetty 11其实都是不支持multipart/related的,之前没有问题是因为CommonsMultipartResolver支持。这样controller直接收到Multipart file这个数据。
6)搭建环境重现、Debug问题。
这里推荐IntelliJ插件jump-to-line
还有个调试技巧是利用IntelliJ的 条件断点及 Evaluate and Log 进行一些变量值的动态修改。
7)修改 org.eclipse.jetty.server.Request,如下
public Collection<Part> getParts()
// if (contentType == null || !MimeTypes.Type.MULTIPART_FORM_DATA.is(HttpField.valueParameters(contentType, null)))
if (contentType == null)
private MultiParts newMultiParts(MultipartConfigElement config, int maxParts) throws IOException
{
// MultiPartFormDataCompliance compliance = getHttpChannel().getHttpConfiguration().getMultipartFormDataCompliance();
MultiPartFormDataCompliance compliance = MultiPartFormDataCompliance.LEGACY;
修改 org.eclipse.jetty.server.MultiPartInputStreamParser 中如下
protected void parse()
// if (_contentType == null || !_contentType.startsWith("multipart/form-data"))
if (_contentType == null)
return;
绕过条件限制后,发现对普通csv文件通过http Multipart/related上传是可以处理了,controller 可以接收到 MultipartFile 类型的 file 参数了。
其实这个蛮侥幸的,如果Jetty代码压根不支持,估计就得再用其它办法了。
8)后来发现zip格式不支持。Debug后发现是Jetty自己在内部处理时,必须要求临时文件的目录要存在,所以有加了对应逻辑。如下:
public void write(String fileName) throws IOException
{
if (_file == null)
{
_temporary = false;
// Make sure the file/directory _tmpDir.getAbsolutePath() + fileName existed.
touchTmpFileForJetty(_tmpDir.getAbsolutePath(), fileName);
这样修改之后通过了QA的测试。
9)为了测试 multipart/related 请求,也颇费周折。
通过curl命令实现了发送 multipart/related 请求。
boundary="upload_boundary"
body=$(cat <<EOF
--$boundary
Content-Disposition: form-data; name="file"; filename="myuploaded.csv"
Content-Type: text/xml; charset=UTF-8
Content-Transfer-Encoding: binary
$(cat /my-path-to-file/my.csv)
--$boundary--
EOF
)
curl -v -X 'POST' \
-H 'accept: application/json' \
-H 'Authorization: YOUR-BASE64-USERIDPWD' \
-H "Content-Type: multipart/related; boundary=$boundary" \
-d "$body" \
'https://Your-Server:Port/service-path'
10) 通过这 curl 命令向spring6 发送没问题。但是向 spring5系统发送后却得到500响应。但是通过java程序发送的multipart/releated请求确没问题。???
后台错误日志:
Caused by: org.apache.commons.fileupload.MultipartStream$MalformedStreamException: Stream ended unexpectedly
11) 为了搞清原因,在本地把 Mitmproxy 跑起来抓包。
从界面上怎么也看不出root cause,直到把请求通过 mitmproxy 导出成curl命令,才发现是换行表示的不同造成的。
通过Java程序发送的能被Spring5处理的请求是\r\n作为换行。
-d '--upload_boundary\x0d\x0aContent-Disposition: form-data; name="file"; filename="myuploaded.csv"\x0d\x0aContent-Type: text;
而curl发送的就是\n。
-d '--upload_boundary\x0aContent-Disposition: form-data; name="file"; filename="myuploaded.csv"\x0aContent-Type: text/xml;
12)为了证实确实是换行符造成的, 把/n转为 /r/n后通过curl命令发送后 Spring5也能处理了。
这个规范 rf7230 上也说有的请求接受者做得更“健壮”可以接受LF结尾的请求。
实际上也就是这些“健壮”破坏了规范。(另外,如果从window系统上用curl命令,应该默认就是CRLF的吧?)
echo $body > body.txt
cat -e body.txt
unix2dos body.txt
cat -e body.txt
RNBody=$(cat body.txt)
echo $RNBody | cat -e
curl -v -X 'POST' \
-H 'accept: application/json' \
-H 'Authorization: YOUR-BASE64-USERIDPWD' \
-H "Content-Type: multipart/related; boundary=$boundary" \
-d "$RNBody" \
'https://Your-Server:Port/service-path'
下面代码演示如何发送zip这样的二进制格式文件。
boundary="upload_boundary"
# 这里使用系统默认回撤换行。
{
echo "--$boundary"
echo "Content-Disposition: form-data; name=\"file\"; filename=\"example.zip\""
echo "Content-Type: text; charset=UTF-8"
echo "Content-Transfer-Encoding: binary"
echo ""
cat /your/path/to/zipfile
echo ""
echo "--$boundary--"
} > zip_body.txt
curl -X POST --proxy http://127.0.0.1:7070 -k \
-H "Content-Type: multipart/related; boundary=$boundary" \
-H "Authorization: Basic XXXXXX" \
--data-binary @zip_body.txt \
https://Your-Server:Port/service-path
# 这里明确使用 \r\n
{
echo -ne "--$boundary\r\n"
echo -ne "Content-Disposition: form-data; name=\"file\"; filename=\"example.zip\"\r\n"
echo -ne "Content-Type: text; charset=UTF-8\r\n"
echo -ne "Content-Transfer-Encoding: binary\r\n"
echo -ne "\r\n"
cat /your/path/to/zipfile
echo -ne "\r\n"
echo -ne "--$boundary--\r\n"
} > zip_body_inCRCL.txt
curl -X POST --proxy http://127.0.0.1:7070 -k \
-H "Content-Type: multipart/related; boundary=$boundary" \
-H "Authorization: Basic XXXXXX" \
--data-binary @zip_body_inCRCL.txt \
https://Your-Server:Port/service-path
13)中间也尝试通过filter在中间使用 Commons FileUpload 2
但是遇到 Stream ended unexpectedly 的问题。另外,在Tomcat做容器的POC中,也是遇到类似的问题。当时debug时发现似乎是跟回车换行有关。当时也都是通过curl命令验证的。但因为自定义Jetty的方案已经可以work,所以就没再继续看。现在回头看很当时遇到的问题很可能跟Spring5遇到的一样。也许发送前对回撤换行处理一下,或许也可以解决。
14)如果通过 Commons FileUpload 2 + Filer 的方式可以解决,那这个方案就是最好的。最不好的方法其实就是这种定制Jetty代码,对以后的升级维护都是潜在的极大风险。
15)想起那句话,重要的是系统要限制能做什么。 为了这个patch前后花费的人天挺多的。。。新版本还delay了好久。
References:
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。