1、简介 {#1简介}
本文将带你了解在 Spring 中处理文件上传(Multipart)请求时出现异常:"No Multipart Boundary Was Found" 的原因,以及解决办法。
2、理解 Multipart 请求 {#2理解-multipart-请求}
简而言之,Multipart 请求是一种 HTTP 请求,它在一条消息的请求体中传输一种或多种不同的数据。请求体被分成多个部分,请求中的每个部分都可能代表不同的文件或数据。
通常使用它来传输或上传文件、交换电子邮件、流媒体或提交 HTML 表单,并使用 Content-Type
标头来指明在请求中发送的数据类型。
来看看 Multipart 请求需要设置哪些值。
2.1、主类型 {#21主类型}
主类型(顶级类型)指定了我们发送的内容的主要类别。如果我们在单个 HTTP 请求中提交多种数据类型,则需要将 Content-Type
Header 值设置为 multipart。
2.2、子类型 {#22子类型}
除了顶级类型外,Content-Type
Header 值还包含一个强制的子类型。子类型值提供了有关数据格式的附加信息。
在不同的 RFC 中定义了多种 multipart 子类型。例如 multipart/mixed
、multipart/alternative
、multipart/related
和 multipart/form-data
(常用)。
由于我们在一个请求中封装了多种不同的数据类型,因此需要一个额外的参数来分隔 multipart 消息的不同部分:即,boundary 参数。
2.3、Boundary 参数 {#23boundary-参数}
Boundary 指令(参数)是 multipart Content-Type
的强制值,它指定了封装边界。
根据 RFC 1341 的定义,封装(Boundary)边界是由两个连字符("--")后跟 Content-Type
Header 中的 boundary
值组成的分隔线。它用于分隔 HTTP 请求体中的各个部分(即,子请求体)。
来看一个实际的案例,Web 浏览器请求包含两个 Part。通常情况下,Content-Type
Header 信息如下:
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryG8vpVejPYc8E16By
封装边界(Boundary)分隔了请求体的每个部分。此外,每个部分都有自己的 Header 部分、一个空行和内容本身。
------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--
最后,在最后一个数据部分之后,有一个结尾边界(Boundary),在结尾处附加了两个连字符。
3、实际案例 {#3实际案例}
现在,创建一个简单的示例,来重现 "no multipart boundary was found" 问题。
如前所述,所有 multipart
请求都必须使用 boundary
参数,因此我们可以选择任何一种 multipart subtype 。为简单起见,我们使用 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";
}
最后,需要选择测试工具进行测试。
3.1、重现问题 {#31重现问题}
在提交表单数据时,cURL 和 Web 浏览器都会自动生成 multipart boundary 。因此,重现该问题的最简单方法是使用 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();
}
尽管我们使用 boundary 分隔了请求体,但在调用用于解析 MediaType
的方法时,我们故意省略了 boundary
值。由于请求头缺少必须值,调用失败。
4、解决这一问题 {#4解决这一问题}
如错误信息所示,问题与 Content-Type
Header 中未设置 boundary
参数有关。
解决这个问题的方法之一是让 Postman 自动生成其值,而不是自己设置 Content-Type
值。这样的话,Postman 就会自动添加以下 Content-Type
Header 信息:
Content-Type: multipart/form-data; boundary=<calculated when request is sent>
如果你想定义自定义 boundary
值,可以这样做:
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());
}
}
这两种情况的解决方案都比较直观,但有几点需要注意。
boundary
参数值是由字母数字(A-Z、a-z、0-9)和特殊字符组成的任意字符串,长度不超过 70 个字符。特殊字符包括 RFC 822 中定义为 "特殊" 的所有字符,以及另外三个字符 "="、"?" 和 "/"。如果使用特殊字符,还必须用引号括起boundary
。- 此外,它必须是唯一的,并且不应出现在请求中发送的任何数据中。
5、总结 {#5总结}
本文介绍了什么是 Multipart 请求,以及在 Spring 中处理文件上传(Multipart)请求时出现异常:"No Multipart Boundary Was Found" 的原因和解决办法。
Web 浏览器、Postman 和 curl 工具可自动生成 multipart boundary。不过,当我们要使用自定义值时,还是需要遵循已定义的规则,以确保在不同系统间正确处理和兼容。
Ref:https://www.baeldung.com/spring-avoid-no-multipart-boundary-was-found