1. 引言
在本教程中,我們將學習如何處理 Spring 中處理多部分 HTTP 消息時常見的“未找到 Multipart Boundary”錯誤。我們將學習如何正確配置此類請求,以防止該問題發生。
2. 理解多部分請求
首先,讓我們定義我們使用的請求類型。簡而言之,多部分請求是包含一個或多個不同類型數據的 HTTP 請求,這些數據位於單個消息的請求體中。 請求體被分割成多個部分,並且在這樣的請求中,每個部分可能代表不同的文件或數據片段。
我們通常使用它來傳輸或上傳文件、交換電子郵件、流式傳輸媒體或提交 HTML 表單,通過設置 Content-Type 標頭來指示請求中發送的數據類型。 讓我們指定需要設置哪些值。
2.1. 頂層類型
頂層類型指定我們發送內容的的主要類別。如果我們在單個 HTTP 請求中提交多種數據類型,則需要將該值設置為 multipart。
另一方面,僅發送一個文件時,應使用 <em >Content-Type</em> 的離散或單部分值之一。
2.2. 子類型
除了頂層類型之外,<em >Content-Type</em> 值還包含一個必選的子類型。子類型值提供有關數據格式的額外信息。
多個 multipart 子類型在不同的 RFC(請求評論)中引入。例如,<em >multipart/mixed</em>、<em >multipart/alternative</em>、<em >multipart/related</em> 和 <em >multipart/form-data</em>。
由於我們正在將多個不同的數據類型封裝在一個請求中,因此我們需要一個額外的參數來區分 multipart 消息的不同部分:邊界參數。
2.3. 邊界參數
邊界指令或參數是必需值,用於 multipart Content-Type。它指定封裝邊界。
根據 RFC 1341,封裝邊界是一個由兩個連字符 (“–“) 組成的行,後跟 boundary 值,該值來自 Content-Type 頭部。它將 HTTP 消息中的各個部分分隔開來。
下面我們來看一個實際例子。在下面的示例中,Web 瀏覽器請求包含兩個主體部分。通常,Content-Type 頭部會像下面這樣:
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryG8vpVejPYc8E16By封裝邊界將分隔身體的各個部分。此外,每個部分都將包含一個標題部分、一個空行以及內容本身。
------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. 實際示例
現在,讓我們重點創建一個簡單的示例,以重現 “未找到 multipart 邊界” 這個問題。
正如之前提到的,所有 multipart 請求都必須使用 boundary 參數,因此我們可以選擇任何 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 和 Web 瀏覽器均會自動生成 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();
}儘管我們已經使用封裝邊界將身體部位分離,但我們在調用用於解析 MediaType 的方法時,故意省略了邊界值。由於請求頭缺少必需值,因此調用將失敗。
4. 解決問題
正如錯誤信息所指示的,問題與 Content-Type 標頭中未設置 boundary 參數有關。
一種解決此問題的辦法是 讓 Postman 自動生成其值,而不是自己設置 Content-Type 值。 這樣,Postman 將自動添加以下 Content-Type 標頭:
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. 防止錯誤的最佳實踐
邊界參數的值是一個任意字符串,最多 70 個字符,包含字母數字字符(A-Z, a-z, 0-9)和特殊字符。 特殊字符包括根據 RFC 822 中“specials”定義的全部字符,以及“=”、“?”、“/”這三個字符。 如果使用特殊字符,則必須用引號將邊界字符串括起來。
此外,它必須是唯一的,並且不能出現在請求中發送的數據中。
遵循這些最佳實踐,可以確保服務器正確解析和解釋邊界字符串。
5. 結論
在本教程中,我們學習瞭如何防止使用多部分請求時常見的錯誤。所有多部分 Content-Type 都需要一個 boundary 參數。
Web 瀏覽器、Postman 和 curl 工具都提供自動生成多部分 boundary 的功能。但是,當我們想要使用自定義值時,我們需要遵循定義的規則,以確保在不同系統上的正確處理和兼容性。