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 Parts, 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.