When a controller obtains files from multipart requests,
there is an inconsistency when jakarta.servlet.http.Part
type is an attribute of an object or an argument of a handler method.
If a method has an object argument, and expects files to be saved to attributes of that argument, the attribute type should not be Part
, but it would rather be MultipartFile
.
This is because StandardMultipartHttpServletRequest
wraps its Part
s, whose filename exists, with StandardMultipartFile
.
StandardMultipartFile
is an private class that implements MultipartFile
class. So Part
type attributes generate conversion errors.
In contrast, if the method has an argument type of Part
, it can normally receive multipart files from the framework.
My idea is simply modifying StandardMultipartFile
class to implement both MultipartFile
and Part
, so preventing conversion issues. Likewise I added new MockStandardMultipartFile
class that replaces MockMultipartFile
class.
Issues: https://github.com/spring-projects/spring-framework/issues/27819
Comment From: pivotal-cla
@binchoo Please sign the Contributor License Agreement!
Click here to manually synchronize the status of this Pull Request.
See the FAQ for frequently asked questions.
Comment From: pivotal-cla
@binchoo Thank you for signing the Contributor License Agreement!
Comment From: rstoyanchev
In the description, you refer to scenario 7 from #15220, but that issue and those scenarios specifically were addressed with PR #370. There is a test class WebRequestDataBinderIntegrationTests for those scenarios. I'm wondering what is the difference between that and what you're running into?
Comment From: binchoo
@rstoyanchev, thanks for your review and comment. The absence of integration test before issuing misled me about the range of this issue. I'm sorry for confusing you.
Scenario 7 doesn't seem to work, specifically, when testing the controller with MockMVC
and MockMultipartHttpServletRequestBuilder
,
while WebRequestDataBinderIntegrationTests demonstrates scenario 7 in integration context.
Please view a failing mockMVC test below.
@Test
public void multipartRequestWithServletPartsForPartAttribute() throws Exception {
byte[] fileContent = "bar".getBytes(StandardCharsets.UTF_8);
MockPart filePart = new MockPart("file", "orig", fileContent);
byte[] json = "{\"name\":\"yeeeah\"}".getBytes(StandardCharsets.UTF_8);
MockPart jsonPart = new MockPart("json", json);
jsonPart.getHeaders().setContentType(MediaType.APPLICATION_JSON);
standaloneSetup(new MultipartController()).build()
.perform(multipart("/partattr").part(filePart).part(jsonPart))
.andExpect(status().isFound()) // expect: 302, actual: 400
.andExpect(model().attribute("fileContent", fileContent))
.andExpect(model().attribute("jsonContent", Collections.singletonMap("name", "yeeeah")));
}
@Controller
private static class MultipartController {
@RequestMapping(value = "/partattr")
public String processPartAttribute(PartForm form,
@RequestPart(required = false) Map<String, String> json, Model model) throws IOException {
if (form != null) {
Part part = form.getFile();
if (0 != part.getSize()) {
byte[] fileContent = StreamUtils.copyToByteArray(part.getInputStream());
model.addAttribute("fileContent", fileContent);
}
}
if (json != null) {
model.addAttribute("jsonContent", json);
}
return "redirect:/index";
}
}
private static class PartForm {
private Part file;
public PartForm(Part file) {
this.file = file;
}
public Part getFile() {
return file;
}
}
Due to type conversion issue described in https://github.com/spring-projects/spring-framework/pull/27830#issue-805125167,
Line 158: I want MockStandardMultipartFile
to be registered to the mock request, so that this can be delivered to Part
type attribute of an object normally.
https://github.com/spring-projects/spring-framework/blob/a1b2262ddad0cd43394aa97d2ecc1b3f37f9f1ad/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMultipartHttpServletRequestBuilder.java#L145-L160
Comment From: rstoyanchev
Thanks for elaborating.
We do support Part
for data binding, but only in fallback mode, when there is no MultipartResolver
(this is in ServletRequestDataBinder
). If you don't want to use MultipartFile
you need to make sure that a MultipartResolver
is not declared.
Comment From: binchoo
@rstoyanchev Thanks! Your comment gave me a lot of hints to deep-dive into multipart bindings.
As I reviewed, - DispatcherServlet::checkMultipart - ServletRequestDataBinder::bind
when a request is a MultipartRequest
that has been resolved by MultipartResolver
, then
- Part
in this request can be resolved for arguments type of Part
and MultipartFile
.
- Part
in this request can be bound to attributes that are MultipartFile
only.
So I concluded that MockMultipartHttpServletRequestBuilder
is doing ok because it is a mock for MultipartRequest
and follows the rules above.
If you don't want to use MultipartFile you need to make sure that a MultipartResolver is not declared.
Yes I could test this by registering a multipart resolver that purposely returns false in isMultiPart()
.
@Bean
MyMultipartResolver multipartResolver() {
return new MyMultipartResolver();
}
class MyMultipartResolver extends StandardServletMultipartResolver {
@Override
public boolean isMultipart(HttpServletRequest request) {
return false;
}
}
Comment From: binchoo
https://github.com/spring-projects/spring-framework/commit/791999610e5a6028bfe8dfe44293beb3f267857c Added tests covering these behaviors.
When a request is a MultipartRequest that has been resolved by MultipartResolver, then - Part in this request can be resolved for arguments type of Part and MultipartFile. -
multipartRequestWithParts_resolvesMultipartFileArguments
-multipartRequestWithParts_resolvesPartArguments
- Part in this request can be bound to attributes that are MultipartFile only. -multipartRequestWithParts_resolvesMultipartFileProperties
-multipartRequestWithParts_cannotResolvePartProperties
Comment From: rstoyanchev
Thanks for the updates. I'll process this.
Note that if you're in Boot application you can use spring.servlet.multipart.enabled=false
to turn off the MultipartResolver
config and then declare a bean of type javax.servlet.MultipartConfigElement
which enables multipart support at the Servlet container level.