I'm trying to create a controller that will consume multipart/mixed requests containing both a json string and a file. I have tried @RequestPart, @RequestParam and using MultipartHttpServletRequest directly, but binding doesn't happen and MultipartHttpServletRequest.getParts() returns an empty collection. However, if the same controller consumes a multipart/form-data request, all approaches work as expected.

Issue https://github.com/spring-projects/spring-boot/issues/30971 mentioned something similar, but it's not the same case and using MultipartHttpServletRequest, as suggested there, doesn't help.

Here's one example using @RequestPart:

@ResponseStatus(HttpStatus.OK)
@PostMapping(
    path = "/test/request-part",
    consumes = {MediaType.MULTIPART_MIXED_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE})
public void testMultipartMixedWithRequestPart(
    @RequestHeader(HttpHeaders.CONTENT_TYPE) String contentType,
    @RequestPart(value = "file", required = false) MultipartFile file,
    @RequestPart(value = "json", required = false) String json) {

    System.out.printf("%s: %s using @RequestPart; file: %s; json: %s%n",
            HttpHeaders.CONTENT_TYPE, contentType, file, json);
}

If I send the following request...

curl -H "Content-Type: multipart/mixed" \
  -F "json={\"name\": \"test\"};type=application/json" \
  -F "file=@dummy-file.txt" \
  http://localhost:8080/test/request-part

... the app will print:

Content-Type: multipart/mixed; boundary=------------------------ea524ebe0a53438e using @RequestParam; file: null; json: null

But if multipart/form-data is used instead, it will work as expected:

Content-Type: multipart/form-data; boundary=------------------------a8bb90b20a18542c using @RequestParam; file: org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@55d9001c; json: {"name": "test"}

I'm using jdk11 and Spring Boot 2.7.9. The repo below has a minimal application to replicate the issue. https://github.com/andrepnh/spring-multipart-mixed-bug

Please let me know if more information is needed.

Comment From: wilkinsona

I think this may be a cURL bug. When it sends a multipart/mixed request, it sets the Content-Disposition to attachment for both parts:

POST /test/multipart-servlet-request HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.88.1
Accept: */*
Content-Length: 335
Content-Type: multipart/mixed; boundary=------------------------b811af1df53480cb

--------------------------b811af1df53480cb
Content-Disposition: attachment; name="json"
Content-Type: application/json

{"name": "test"}
--------------------------b811af1df53480cb
Content-Disposition: attachment; name="file"; filename="dummy-fi
le.txt"
Content-Type: text/plain


--------------------------b811af1df53480cb--

This content disposition prevents Tomcat from identifying the names of the parts as it looks for form-data:

Content-Type: multipart/mixed; boundary=------------------------b811af1df53480cb using MultipartHttpServletRequest; amount of parts on request: 0

If I modify the request that's sent to using form-data as the content disposition, it works as expected:

POST /test/multipart-servlet-request HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.88.1
Accept: */*
Content-Length: 335
Content-Type: multipart/mixed; boundary=------------------------b811af1df53480cb

--------------------------b811af1df53480cb
Content-Disposition: form-data; name="json"
Content-Type: application/json

{"name": "test"}
--------------------------b811af1df53480cb
Content-Disposition: form-data; name="file"; filename="dummy-fi
le.txt"
Content-Type: text/plain


--------------------------b811af1df53480cb--

Content-Type: multipart/mixed; boundary=------------------------b811af1df53480cb using MultipartHttpServletRequest; amount of parts on request: 2

This appears to be a variation of https://github.com/curl/curl/issues/5256.

If you disagree and believe that cURL's behaviour is correct, please follow up with the Tomcat community as it's code in Tomcat that's responsible for parsing the request.