避免春季“未找到多部分邊界”
一、簡介
在本教程中,我們將了解在 Spring 中處理多部分 HTTP 訊息時常見的錯誤「未找到多部分邊界」。我們將學習如何正確配置此類請求以防止問題發生。
2. 理解多部分請求
首先,讓我們定義將使用的請求類型。簡而言之,多部分請求是 HTTP 請求,它在單一訊息正文中傳輸一種或多種不同類型的資料。有效負載被分成多個部分,此類請求中的每個部分可能代表不同的檔案或資料。
我們通常使用它來傳輸或上傳文件、交換電子郵件、串流媒體或提交 HTML 表單,並使用Content-Type
標頭來指示我們在請求中發送的資料類型。讓我們指定需要在那裡設定哪些值。
2.1.頂級類型
頂級類型指定我們發送的內容的主要類別。如果我們在單一 HTTP 請求中提交多種資料類型,則需要將該值設為 multipart。
另一方面,當僅發送一個檔案時,我們應該使用Content-Type
的離散或單部分值之一。
2.2.亞型
除了頂級類型之外, Content-Type
值還包含強制子類型。子類型值提供有關資料格式的附加資訊。
不同的 RFC(徵求意見)中引入了多個多部分子類型。範例包括multipart/mixed
、 multipart/alternative
、 multipart/related
和multipart/form-data
。
由於我們在單一請求中封裝了多種不同的資料類型,因此我們需要一個附加參數來分隔多部分訊息的不同部分:邊界參數。
2.3.邊界參數
邊界指令或參數是多部分Content-Type
的強制值。它指定封裝邊界。
根據RFC 1341中的定義,封裝邊界是由兩個連字號 (“–”) 後面接著Content-Type
header 中的boundary
值所組成的分隔符號行。它將 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. 實際例子
現在讓我們重點建立一個簡單的範例,該範例將重現“no multipart boundary was found”
問題。
如前所述,所有多部分請求都必須使用邊界參數,因此我們可以選擇任何多部分子類型。為了簡單起見,我們使用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 瀏覽器都會自動產生多部分邊界。因此,重現問題的最簡單方法是使用 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
標頭中設定邊界參數有關。
解決該問題的一種方法是讓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.防止錯誤的最佳實踐
邊界參數值是由字母數字(AZ、az、0-9)和不超過70個字元的特殊字元組成的任意字串。特殊字符包括RFC 822中定義為“特殊字符”的所有字符,以及附加的三個字符“=”、“?”和“/”。如果我們使用特殊字符,我們還必須將邊界括在引號中。
此外,它必須是唯一的,並且不應出現在請求中發送的任何資料中。
透過遵循最佳實踐,我們確保伺服器正確解析和解釋邊界字串。
5. 結論
在本教程中,我們了解如何防止使用多部分請求時出現的常見錯誤。所有多部分Content-Type
都需要邊界參數。
Web 瀏覽器、Postman 和curl 工具提供自動產生多部分邊界。儘管如此,當我們想要使用任意值時,我們需要遵循定義的規則集,以確保不同系統之間的正確處理和相容性。
與往常一樣,本文中使用的程式碼範例可在 GitHub 上取得。