I'm implementing a REST service with Spring Boot to upload a file making a Multipart POST request. I set a maximum file size in the application.yml as below:

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

My service also implements a CORS policy to allow requests from a client app hosted by a development React server:

    @EnableWebSecurity
    @EnableMethodSecurity
    @Configuration
    public class SecurityConfig implements WebMvcConfigurer {

        @Value("${security.cors-origin:}")
        private String allowedOrigin;

        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtDecoder decoder, HandlerMappingIntrospector introspector) throws Exception {

            return http
                    .csrf(httpSecurityCsrfConfigurer -> httpSecurityCsrfConfigurer.disable())
                    .authorizeHttpRequests(authCustomizer -> authCustomizer.requestMatchers(new MvcRequestMatcher(introspector, "/actuator/**")).permitAll())
                    .authorizeHttpRequests(authCustomizer -> authCustomizer.requestMatchers(new MvcRequestMatcher(introspector, "/integration/**")).permitAll())
                    .authorizeHttpRequests(authCustomizer -> authCustomizer.anyRequest().authenticated())
                    .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.decoder(decoder)))
                    .sessionManagement(sessionCustomizer -> sessionCustomizer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                    .build();
        }

        /**
         * Add a specified host to the allowed origin for Cross-Origin Resource Sharing
         *
         * This is needed in development as the origin of the front-end (localhost:3000)
         * is different to the origin of the back-end (localhost:8080)
         *
         * @param registry the CorsRegistry to use to add the allowed origin
         */
        @Override
        @ConditionalOnProperty(prefix = "security", name="cors-origin")
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/**")
                    .allowedOrigins(allowedOrigin)
                    .allowCredentials(true)
            ;
        }

    }

I also have a ControllerAdvice to handle exceptions thrown by controllers and Spring:

        @RestControllerAdvice
        @Slf4j
        public class GlobalControllerExceptionHandler extends ResponseEntityExceptionHandler {

            //...

            @ExceptionHandler({ MaxUploadSizeExceededException.class })
            @ResponseStatus(value = HttpStatus.BAD_REQUEST)
            public FileUploadErrorMessage handleMaxUploadSizeExceededException(MaxUploadSizeExceededException ex, WebRequest request) {
                FileUploadErrorMessage errorMessage = new FileUploadErrorMessage("inputValidation", "invalid file");
                ValidationResult validationResult = new ValidationResult("invalid.logo.size");
                errorMessage.setErrors(new ArrayList<>(Collections.singleton(validationResult)));

                return  errorMessage;
            }                                    
    }

The problem is that with the MaxUploadSizeExceededException, Spring Web doesn't add the Access-Control-Allow-Origin header to the error response. It works fine for other (controllers thrown) exceptions. I thought it was because the request doesn't go through any controllers as it's blocked by the framework (which throws the exception), but the weird thing is that if I don't handle MaxUploadSizeExceededException in the above ControllerAdvice, Spring returns a 500 Internal error with a response body and the CORS header as well!

I'm using Spring Boot 3.1.3

Comment From: sdeleuze

Could you please try with Spring Boot 3.1.4 since we did recently some changes in this area? If you can still reproduce, could you please attach an archive or share a repository with a ready-to-use reproducer?

Comment From: giodas

Could you please try with Spring Boot 3.1.4 since we did recently some changes in this area? If you can still reproduce, could you please attach an archive or share a repository with a ready-to-use reproducer?

I see the same behaviour with 3.1.4. I'll put together a reproducer as soon as I can.

Comment From: jchenga

I have a sample application that reproduces the issue with Spring Boot 3.1.4. the repository is at https://github.com/jchenga/CorsErrorMaxUploadSize

After starting the application, you can open the testupload.html in a browser to upload a file bigger than 1MB. Access-Control-Allow-Origin does not appear in the response headers with the MaxUploadSizeExceededException.

if RestControllerAdvice is disabled, Access-Control-Allow-Origin appears in the response headers.

Comment From: sdeleuze

Could you please check the repro for the regular use case that is expected to work, I keep having error 500 and would like to be sure we start from a base use case that works to analyse what's going on for the error use case.

Comment From: spring-projects-issues

If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.

Comment From: jchenga

I've fixed the error 500, and the base use case works now. please pull the latest.

Comment From: sdeleuze

Even if the exception comes from a pretty low level handling, without @ExceptionHandler the CORS headers are indeed added to the response when a MaxUploadSizeExceededException is thrown by StandardMultipartHttpServletRequest. This is because the regular handling still happens in that case.

But when a matching @ExceptionHandler is configured, the related HandlerExceptionResolver handle the request, so DispatcherServlet consider the exchange fully handled, hence skipping our CORS handler/interceptor.

Handling such use case would require some duplication and maintenance burden for a very specific use case (exception occurs before a handler is selected), so we recommend to configure a CorsFilter instead of using Spring MVC global CORS configuration since it works as expected with your use case, for example with:

@Bean
CorsFilter corsFilter() {
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    CorsConfiguration config = new CorsConfiguration().applyPermitDefaultValues();
    source.registerCorsConfiguration("/**", config);
    return new CorsFilter(source);
}