'How to limit MultipartFile size per endpoint or controller (NOT globally)?

I want to set a unique file size limit per endpoint or per controller.

I am aware I can set a global file size limit with the following properties, but this is NOT what I want to do, I want UNIQUE LIMIT PER ENDPOINT or CONTROLLER:

spring:
  servlet:
    multipart:
      max-file-size: 1MB
      max-request-size: 1MB

I have created a custom validator, but that is not good enough, I want to prevent the user from calling the endpoint rather than throw away files that are too large, I want to prevent the upload.

public class LogoValidator implements ConstraintValidator<ValidLogo, MultipartFile> {

    private static final String[] validLogoTypes = new String[]{"image/jpg", "image/jpeg", "image/png", "image/svg"};
    private static final long MAX_LOGO_SIZE = 1024L * 1024L;

    @Override
    public boolean isValid(MultipartFile file, ConstraintValidatorContext context) {

        if (file.getSize() > MAX_LOGO_SIZE) {
            return false;
        }

        for(String type : validLogoTypes) {
            if (type.equals(file.getContentType())) return true;
        }

        return false;
    }

}

@Documented
@Constraint(validatedBy = LogoValidator.class)
@Target( {ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidLogo {
    String message() default "Invalid logo.";
}


Solution 1:[1]

Based on the tips provided by swapyonubuntu, create a bean to register a new filter. Then register your filter, may as well implement inline if this specific filter is only used for a very specific endpoint or two.

@Configuration
public class LogoFilterConfig {

    private static final List<String> VALID_LOGO_TYPES = Arrays.asList(
            "image/jpeg", "image/jpg", "image/svg", "image/png"
    );
    private static final long MAX_IMG_SIZE = 2000000;

    @Bean
    public FilterRegistrationBean<Filter> logoFilter(){
        FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>();

        registrationBean.setFilter(new OncePerRequestFilter() {

            @Override
            protected void doFilterInternal(
                    HttpServletRequest request, HttpServletResponse response, FilterChain filterChain
            ) throws ServletException, IOException {

                if (!VALID_LOGO_TYPES.contains(request.getPart("logo").getContentType())) {
                    response.sendError(HttpStatus.BAD_REQUEST.value(), "Unsupported media type.");
                    return;
                    /*
                        Returning here means the req/resp is never added to filterChain.
                        
                        The 'response' is returned to the consumer in its current state, 
                        therefore it's best to modify it for better ux.
                     */
                }

                if (request.getPart("logo").getSize() > MAX_IMG_SIZE) {
                    response.sendError(HttpStatus.BAD_REQUEST.value(), "Logo too large, max allowed 2MB.");
                    return;
                }

                /*
                    Counterintuitively adding to the filter chain is how you allow the request to go through.
                */
                filterChain.doFilter(request, response);
            }

        });
        registrationBean.addUrlPatterns("/my/logo");
        registrationBean.setOrder(2);

        return registrationBean;
    }

}

Solution 2:[2]

You can try doing same using Servlet Filters.Here since you are using spring, OncePerRequestFilter can be great choice.

https://www.baeldung.com/spring-onceperrequestfilter

In filter you can bind size per endpoint. There are various ways to do that eg: you can make it configurable via configuration yaml.

endpoints:
    /some/endpt: 
        supported-types: yaml,image
        max-size:300
    /some/other/endpt: 
        supported-types:image
        max-size:300

Wire this to a class using configurationproperties https://www.baeldung.com/configuration-properties-in-spring-boot#nested-properties

Initialize at start of filter 

Map<String,String> endpointMap = new HashMap<>();
endpointMap.putAll(fromSpringconfigurationpropertiesmap)

//use request.getUrl() or request.getServletPath() based on your requirement to get url

endpointMap.get("urlFromRequest"); // can return url+ supported types.
//reject if size and supported type does not match logic here...

Hope it helps.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Igor Flakiewicz
Solution 2 swapyonubuntu