1. Introduction

In this tutorial, we’ll learn about the common error “No Multipart Boundary Was Found” when handling multipart HTTP messages in Spring. We’ll learn how to properly configure such requests to prevent the issue from occurring.

2. Understanding Multipart Requests

First, let’s define the type of requests we’ll use. In short, a multipart request is an HTTP request that transfers one or more different types of data within the body of a single message. The payload is split into parts, and each part in such a request may represent a different file or piece of data.

We commonly use it to transfer or upload files, exchange emails, stream media, or submit HTML forms, using the Content-Type header to indicate the data type we’re sending in the request. Let’s specify which values we need to set there.

2.1. Top-Level Type

The top-level type specifies the main category of the content we’re sending. We need to set the value to multipart if we submit various data types in a single HTTP request.

On the other hand, when sending only one file, we should use one of the discrete or single-part values of a Content-Type.

2.2. Subtypes

Besides a top-level type, the Content-Type value also contains a mandatory subtype. The subtype value provides additional information about the format of the data.

Several multipart subtypes are introduced across different RFCs (Request for Comments). Examples include multipart/mixed, multipart/alternative, multipart/related, and multipart/form-data.

Since we’re encapsulating multiple distinct data types within a single request, we need one additional parameter to separate the different parts of the multipart message: the boundary parameter.

2.3. The Boundary Parameter

The boundary directive or parameter is a mandatory value for multipart Content-Type. It specifies the encapsulation boundary.

As defined in RFC 1341, the encapsulation boundary is a delimiter line consisting of two hyphen characters (“–“) followed by the boundary value from the Content-Type header. It separates the various parts within the HTTP message.

Let’s see this in action. In the example below, the web browser request contains two body parts. Typically, the Content-Type header will look like:

Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryG8vpVejPYc8E16By

An encapsulation boundary will separate each part of the body. Moreover, each part will have a header section, a blank line, and the content itself.

------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--

Finally, after the last data part, there’s a closing boundary with two additional hyphen characters appended to the end.

3. Practical Example

Let’s now focus on creating a simple example that will reproduce the “no multipart boundary was found” issue.

As previously mentioned, all multipart requests must use the boundary parameter, so we can choose any of the multipart subtypes. For simplicity, let’s use multipart/form-data.

First, let’s create a form that accepts two different types of data — a file and its textual description:

<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>

The enctype attribute specifies how the browser should encode the form data when submitted.

Next, we’ll expose a REST endpoint:

@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String upload(@RequestParam("file") MultipartFile file, String fileDescription) {
    return "files/success";
}

The method handles HTTP POST requests and accepts two parameters matching the input of our form. By defining a consumes attribute, we specify
the expected content type.

Lastly, we need to choose the testing tool.

3.1. Reproducing the Issue

Both curl and web browsers automatically generate the multipart boundary when submitting form data. Therefore, the simplest way to reproduce the issue is by using Postman.

If we set Content-Type to just multipart/form-data, we’ll receive the following response:

{
    "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"
}

Let’s also create a unit test using OkHttp to reproduce the same outcome:

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();
}

Even though we’ve separated the body parts using the encapsulation boundary, we’ve intentionally omitted the boundary value when invoking the method used for parsing the MediaType. Since the request header is missing the mandatory value, the call will fail.

4. Resolving the Issue

As the error message suggests, the issue is related to the boundary parameter not being set in the Content-Type header.

One way to resolve the issue is to let Postman automatically generate its value, not setting the Content-Type value ourselves. That way, Postman automatically adds the following Content-Type header:

Content-Type: multipart/form-data; boundary=<calculated when request is sent>

On the other hand, if we want to define a custom boundary value, we can do it like this:

Content-Type: multipart/form-data; boundary=PlaceOurCustomBoundaryValueHere

Similarly, we can add a unit test to cover the successful scenario:

@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());
    }
}

The solution is relatively intuitive in both cases, but there are a few things to keep in mind.

4.1. Best Practices to Prevent the Error

The boundary parameter value is an arbitrary string consisting of alphanumeric (A-Z, a-z, 0-9) and special characters no longer than 70 characters. Special characters include all characters defined as the “specials” in RFC 822, with the additional three characters “=”, “?”, and “/”. If we use special characters, we must also enclose the boundary in quotes.

Additionally, it must be unique and should not occur in any of the data sent in the request.

By following the best practices, we ensure the server correctly parses and interprets the boundary string.

5. Conclusion

In this tutorial, we’ve seen how to prevent the common error when using multipart requests. All multipart Content-Type require a boundary parameter.

Web browsers, Postman, and curl tools offer automatic generation of the multipart boundary. Still, when we want to use an arbitrary value, we need to follow the defined set of rules to ensure proper handling and compatibility across different systems.

As always, the code samples used in this article are available over on GitHub.