1. 引言
本教程将探讨处理Spring中multipart HTTP请求时常见的"No Multipart Boundary Was Found"错误。我们将学习如何正确配置这类请求,避免踩坑。
2. 理解Multipart请求
首先明确我们要处理的请求类型。multipart请求是一种在单个HTTP消息体中传输多种不同类型数据的请求。其有效载荷被分割成多个部分,每个部分可能代表不同的文件或数据块。
这类请求常用于文件上传、邮件传输、媒体流传输或HTML表单提交,通过Content-Type
头标识请求数据类型。下面我们详细说明需要设置的参数值。
2.1. 顶层类型
顶层类型指定我们发送内容的主要类别。当在单个HTTP请求中提交多种数据类型时,必须将值设置为multipart
。
⚠️ 注意:仅发送单个文件时,应使用Content-Type
的离散类型(如image/jpeg
),而非multipart
。
2.2. 子类型
除顶层类型外,Content-Type
值还包含必填的子类型。子类型提供数据格式的额外信息。
不同RFC文档定义了多种multipart子类型,常见包括:
multipart/mixed
multipart/alternative
multipart/related
multipart/form-data
由于我们在单个请求中封装多种数据类型,需要额外参数分隔multipart消息的不同部分:边界参数(boundary)。
2.3. 边界参数
边界参数是multipart Content-Type
的必填值,用于指定封装边界。
根据RFC 1341定义,封装边界是由两个连字符(--
)后跟Content-Type
头中的boundary
值组成的分隔行,用于分隔HTTP消息中的各个部分。
看实际例子。以下浏览器请求包含两个body部分,典型的Content-Type
头如下:
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryG8vpVejPYc8E16By
每个body部分由封装边界分隔,且每个部分包含:
- 头部区域
- 空行
- 内容本身
------WebKitFormBoundaryG8vpVejPYc8E16By
Content-Disposition: form-data; name="file"; filename="import.csv"
Content-Type: text/csv
content-of-the-csv-file
------WebKitFormBoundaryG8vpVejPYc8E16By
Content-Disposition: form-data; name="fileDescription"
Records
------WebKitFormBoundaryG8vpVejPYc8E16By--
最后,最后一个数据部分后是结束边界,末尾附加两个额外连字符。
3. 实战示例
现在我们创建一个简单示例复现"no multipart boundary was found"问题。
如前所述,所有multipart请求必须使用边界参数,因此可选择任意multipart子类型。为简单起见,使用multipart/form-data
。
首先创建一个接受两种数据类型(文件及其文本描述)的表单:
<form th:action="@{/files}" method="POST" enctype="multipart/form-data">
<label for="file">File to upload:</label>
<input type="file" id="file" name="file" required>
<label for="fileDescription">File description:</label>
<input type="text" id="fileDescription" name="fileDescription" placeholder="Description" required>
<button type="submit">Upload</button>
</form>
enctype
属性指定浏览器提交表单时的编码方式。
接着暴露一个REST接口:
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String upload(@RequestParam("file") MultipartFile file, String fileDescription) {
return "files/success";
}
该方法处理HTTP POST请求,接受两个与表单输入匹配的参数。通过定义consumes
属性指定预期内容类型。
最后选择测试工具。
3.1. 复现问题
curl和浏览器在提交表单数据时会自动生成multipart边界。因此复现问题的最简单方式是使用Postman。
若仅将Content-Type
设置为multipart/form-data
,会收到以下响应:
{
"timestamp": "2024-05-01T10:10:10.100+00:00",
"status": 500,
"error": "Internal Server Error",
"trace": "org.springframework.web.multipart.MultipartException: Failed to parse multipart servlet request... Caused by: org.apache.tomcat.util.http.fileupload.FileUploadException: the request was rejected because no multipart boundary was found... 43 more\n",
"message": "Failed to parse multipart servlet request",
"path": "/files"
}
使用OkHttp创建单元测试复现相同结果:
private static final String BOUNDARY = "OurCustomBoundaryValue";
private static final String BODY =
"--" + BOUNDARY + "\r\n" +
"Content-Disposition: form-data; name=\"file\"; filename=\"import.csv\"\r\n" +
"Content-Type: text/csv\r\n" +
"\r\n" +
"content-of-the-csv-file\r\n" +
"--" + BOUNDARY + "\r\n" +
"Content-Disposition: form-data; name=\"fileDescription\"\r\n" +
"\r\n" +
"Records\r\n" +
"--" + BOUNDARY + "--";
@Test
void givenFormData_whenPostWithoutBoundary_thenReturn500() throws IOException {
RequestBody requestBody = RequestBody.create(BODY.getBytes(), parse(MediaType.MULTIPART_FORM_DATA_VALUE));
try (Response response = executeCall(requestBody)) {
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR.value(), response.code());
}
}
private Response executeCall(RequestBody requestBody) throws IOException {
Request request = new Request.Builder().url(HOST + port + FILES)
.post(requestBody)
.build();
return new OkHttpClient().newCall(request)
.execute();
}
尽管我们使用封装边界分隔了body部分,但在调用解析MediaType
的方法时故意省略了边界值。由于请求头缺少必填值,调用将失败。
4. 解决问题
错误信息明确指出问题在于Content-Type
头中未设置边界参数。
解决方案有两种:
✅ 方案一:让Postman自动生成边界值
- 不手动设置
Content-Type
,Postman会自动添加:Content-Type: multipart/form-data; boundary=<calculated when request is sent>
✅ 方案二:自定义边界值
Content-Type: multipart/form-data; boundary=PlaceOurCustomBoundaryValueHere
同样,添加单元测试覆盖成功场景:
@Test
void givenFormData_whenPostWithBoundary_thenReturn200() throws IOException {
RequestBody requestBody = RequestBody.create(BODY.getBytes(), parse(MediaType.MULTIPART_FORM_DATA_VALUE + "; boundary=" + BOUNDARY));
try (Response response = executeCall(requestBody)) {
assertEquals(HttpStatus.OK.value(), response.code());
}
}
两种方案都很直观,但需注意以下最佳实践。
4.1. 避免错误的最佳实践
边界参数值需满足以下规则:
- 由字母数字(A-Z, a-z, 0-9)和特殊字符组成的任意字符串
- 长度不超过70字符
- 特殊字符包括RFC 822定义的"特殊字符"及
=
、?
、/
- 使用特殊字符时必须用引号包裹边界值
⚠️ 关键要求:边界值必须唯一,且不能出现在请求数据的任何部分。
遵循这些最佳实践可确保服务器正确解析边界字符串。
5. 总结
本教程展示了如何避免使用multipart请求时的常见错误。所有multipart Content-Type
都需要边界参数。
浏览器、Postman和curl工具提供自动生成multipart边界的功能。但当需要自定义值时,必须遵循既定规则以确保跨系统的正确处理和兼容性。
本文使用的代码示例可在GitHub获取。