MockMultipartHttpServletRequest and MockMultipartHttpServletRequestBuilder Behavior have been found with the behavior of MockMultipartHttpServletRequest (spring-test) and MockMultipartHttpServletRequestBuilder (spring-test).
MockMultipartHttpServletRequest internally manages multipartFiles as separate file types. When using MockMultipartHttpServletRequestBuilder to create a mock multipart request, files can be added using either the .part method to add a MockPart or the .file method to add a MockMultipartFile.
this.mockMvc.perform(multipart("/upload")
.file(new MockMultipartFile("file", "file.pdf", "application/pdf", "sample content".getBytes()))
.part(new MockPart("file", "test.pdf", "contents".getBytes(), MediaType.APPLICATION_PDF))
);
The actual servlet creation process is implemented as follows:
protected final MockHttpServletRequest createServletRequest(ServletContext servletContext) {
MockMultipartHttpServletRequest request = new MockMultipartHttpServletRequest(servletContext);
Charset defaultCharset = (request.getCharacterEncoding() != null ?
Charset.forName(request.getCharacterEncoding()) : StandardCharsets.UTF_8);
this.files.forEach(request::addFile);
this.parts.values().stream().flatMap(Collection::stream).forEach(part -> {
request.addPart(part);
try {
String name = part.getName();
String filename = part.getSubmittedFileName();
InputStream is = part.getInputStream();
if (filename != null) {
request.addFile(new MockMultipartFile(name, filename, part.getContentType(), is));
} else {
InputStreamReader reader = new InputStreamReader(is, getCharsetOrDefault(part, defaultCharset));
String value = FileCopyUtils.copyToString(reader);
request.addParameter(part.getName(), value);
}
} catch (IOException ex) {
throw new IllegalStateException("Failed to read content for part " + part.getName(), ex);
}
});
}
As can be seen in the code, files added using the .file method are only processed using addFile. However, files added using the .part method are processed using both addPart and addFile, resulting in duplicate data being included in the servlet creation.
Spring Server and Multipart Data Processing
Spring servers can process multipart data in two ways:
@RequestParam("file") MultipartFile file
request.multipartData().getFirst("file")
In particular, when using Functional Endpoints, only the second method is available. An example is shown below:
@RestController
class FileUploadController {
@PostMapping("/upload")
public ReturnObject handleFileUpload(@RequestParam("file") MultipartFile file) {
System.out.println("Received file: " + file.getOriginalFilename());
return new ReturnObject("File uploaded successfully");
}
}
@Component
class FileUploadFunctionalEndpoint {
@Bean
public RouterFunction<ServerResponse> fileUploadRoute() {
return RouterFunctions.route(
POST("/upload"),
request -> {
var filePart = request.multipartData().getFirst("file");
var file = (Part) filePart;
System.out.println("Received file: " + file.getSubmittedFileName());
return ServerResponse.ok().body(new ReturnObject("File uploaded successfully"));
}
);
}
}
Spring RestDocs and MockPart Issues
When using RestDocs to test, files added using the .part method result in the following output:
this.mockMvc.perform(multipart("/upload")
.part(new MockPart("file", "test.pdf", "contents".getBytes(), MediaType.APPLICATION_PDF)))
.andDo(document("upload",
responseFields(
fieldWithPath("message").description("A message")
)
));
$ curl 'http://localhost:8080/upload' -i -X POST \
-H 'Content-Type: multipart/form-data;charset=UTF-8' \
-F 'file=@test.pdf;type=application/pdf' \
-F 'file=@test.pdf;type=application/pdf'
As shown in the output, even though only one file was added using the .part method, the file is included twice in the request.
Review Points
- When using
.file to build a MockMultipartFile, the request's part data is empty, making it impossible to read the data using Functional Endpoints. Is it appropriate to add part data to the request in tests as well?
- As far as i know HTTP Multipart requests use
Part normally. Therefore, it seems more consistent to fill in the part data for mock objects as well.
- I think, This issue is not limited to
RestDocs alone but appears to be a problem stemming from the subtle differences in behavior between various modules (Spring, spring-test(MockMVC), spring-restdocs, etc.).
Proposed Solution
Currently, MockMvcRequestConverter (Spring RestDocs) dependency that inspects and processes MockMultipartHttpServletRequest instances. to process "file" because parts is emply when you use .file() for uploading Multipart
IMO, this seems to be the case when using the common Spring method @RequestParam("file") MultipartFile file to upload files.
Solution:
-
Handle within RestDocs
Since MockMvcRequestConverter is already dependent on MockMultipartHttpServletRequest, it would be more suitable to handle this issue within RestDocs rather than modifying other modules and potentially breaking backward compatibility.
-
Remove duplicates
Since MockMultipartHttpServletRequestBuilder always adds both File and Part when addPart with filename is called,
When the same file is added to both File and Part, remove the duplicate.
-
Duplicate validation
There may be cases where multiple files with the same name need to be included intentionally.
Therefore, the duplicate removal logic should only apply when File and Part exist as a pair.
By implementing this solution, the issue of duplicate file parts being generated in MockMultipartHttpServletRequest can be resolved, ensuring that RestDocs generates accurate API documentation.
MockMultipartHttpServletRequest and MockMultipartHttpServletRequestBuilder Behavior have been found with the behavior of
MockMultipartHttpServletRequest(spring-test) andMockMultipartHttpServletRequestBuilder(spring-test).MockMultipartHttpServletRequestinternally managesmultipartFilesas separate file types. When usingMockMultipartHttpServletRequestBuilderto create a mock multipart request, files can be added using either the.partmethod to add aMockPartor the.filemethod to add aMockMultipartFile.The actual servlet creation process is implemented as follows:
As can be seen in the code, files added using the
.filemethod are only processed usingaddFile. However, files added using the.partmethod are processed using bothaddPartandaddFile, resulting in duplicate data being included in the servlet creation.Spring Server and Multipart Data Processing
Spring servers can process multipart data in two ways:
@RequestParam("file") MultipartFile filerequest.multipartData().getFirst("file")In particular, when using Functional Endpoints, only the second method is available. An example is shown below:
Spring RestDocs and MockPart Issues
When using
RestDocsto test, files added using the.partmethod result in the following output:As shown in the output, even though only one file was added using the
.partmethod, the file is included twice in the request.Review Points
.fileto build aMockMultipartFile, the request's part data is empty, making it impossible to read the data using Functional Endpoints. Is it appropriate to add part data to the request in tests as well?Partnormally. Therefore, it seems more consistent to fill in the part data for mock objects as well.RestDocsalone but appears to be a problem stemming from the subtle differences in behavior between various modules (Spring,spring-test(MockMVC),spring-restdocs, etc.).Proposed Solution
Currently,
MockMvcRequestConverter(Spring RestDocs) dependency that inspects and processesMockMultipartHttpServletRequestinstances. to process "file" because parts is emply when you use .file() for uploading MultipartIMO, this seems to be the case when using the common Spring method @RequestParam("file") MultipartFile file to upload files.
Solution:
Handle within RestDocs
Since
MockMvcRequestConverteris already dependent onMockMultipartHttpServletRequest, it would be more suitable to handle this issue withinRestDocsrather than modifying other modules and potentially breaking backward compatibility.Remove duplicates
Since
MockMultipartHttpServletRequestBuilderalways adds bothFileandPartwhenaddPart with filenameis called,When the same file is added to both
FileandPart, remove the duplicate.Duplicate validation
There may be cases where multiple files with the same name need to be included intentionally.
Therefore, the duplicate removal logic should only apply when
FileandPartexist as a pair.By implementing this solution, the issue of duplicate file parts being generated in
MockMultipartHttpServletRequestcan be resolved, ensuring thatRestDocsgenerates accurate API documentation.