知識庫 / Spring / Spring MVC RSS 訂閱

避免“未找到 Multipart Boundary”在 Spring 中出現

Spring MVC
HongKong
12
11:08 AM · Dec 06 ,2025

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 的功能。但是,當我們想要使用自定義值時,我們需要遵循定義的規則,以確保在不同系統上的正確處理和兼容性。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.