Affects: 3.2.0


My project is now getting "403 Invalid CORS Request" responses when sending an OPTIONS request with the "access-control-request-method" header. It worked fine in version 3.1.6.

"access-control-request-method" is a valid CORS header so I'm not sure why this error is surfacing. Two other CORS headers that I use are "origin" and "access-control-request-headers" and they cause no issues. I can send any combination of those two headers and I get the correct responses.

One way around this is to add a custom CorsMapping like such:

@EnableWebMvc
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

  @Override
  public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/**")
        .allowedMethods("*")
        .allowedOrigins("*")
        .allowedHeaders("*");
  }
  ```

However, I don't want to do this as I have my own custom CORSInterceptor that I want to handle OPTIONS requests. The above solution does things I don't want, such as setting the response header of **access-control-allow-origin: "*"**

Did something change that specifically causes issues with just this header? 

**Comment From: bclozel**

Hello @rcolombo - unfortunately, there isn't enough information here to help you. I'm not seeing any specific CORS-related change in the 6.1.x line.

Can you given an example of request that is being rejected? You're also mentioning a custom CORS interceptor and we're not seeing it here. Can you share a minimal sample application that reproduces the problem? 

**Comment From: rstoyanchev**

You could place a debug point in `DefaultCorsProcessor`, or otherwise provide a minimal sample. 

**Comment From: rcolombo**

Perhaps this is user error. Thank you for pointing me to the `DefaultCorsProcessor`.

This line rejects my request because I have a preflight request but a null config: https://github.com/spring-projects/spring-framework/blob/main/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java#L85

I'm still unclear why I didn't see this until version 3.2.0 as the `DefaultCorsProcessor` has been stable for some time. Apologies if this was improperly filed as an issue - there must be something else in my setup that has this working for 3.1.6 and not 3.2.0.

The fix that worked for my exact use case was to implement my CORS Interceptor as a CORS Filter instead. This way, I am able to add CORS response headers before `DefaultCorsProcessor` processes the request (effectively neutering that processor).

My overall goal is to skip/disable the built-in Spring CORS Processor, as my exact use case requires more complicated processing.

**Comment From: sdeleuze**

Based on your feedback, I close this issue.

If you want to skip Spring processing, putting a custom CORS filter will indeed likely disable Spring own CORS processing if it sets CORS response headers.

Be aware you also could probably instead provide your own `CorsProcessor` via `org.springframework.web.servlet.handler.AbstractHandlerMapping#setCorsProcessor`.

**Comment From: kenips**

@sdeleuze @rcolombo this is a valid issue even when original poster decided to move away from built-in processor.

The issue stems from https://github.com/spring-projects/spring-framework/blob/main/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java#L96-104:

if (config == null) { if (preFlightRequest) { rejectRequest(new ServletServerHttpResponse(response)); return false; } else { return true; } }

where when one does not supply the config, it is assumed that CORS is not enabled and hence you return true. However the block above create a situation where you reject the pre-flight even when config is null. You end up having users reporting issues that only a combination of headers in pre-flight would trigger this, like this here: https://github.com/spring-cloud/spring-cloud-gateway/issues/112.

For my use case, I'd like pre-flight AND actual calls to all route to target server in Spring Cloud Gateway, and the expectation is that I can disable pre-flight rejection here as well. When I start supplying a config to make this work, I ended up having CORS check both at my gateway AND my target app, resulting duplicated `Access-Control-Allow-Origin` on my final response.

As it currently stands, I also end up rolling my own CorsFilter to allow pre-flight at gateway only, and letting target servers to handle actual.

**Comment From: workflo**

I had a very similar problem: I'm migrating a huge legacy app (20+ years of development ;) ) to using Spring Web bit by bit.
As we already have custom code that handles OPTIONS and especially preflight requests I don't want to interfere Spring in handling them AT ALL.

I ended up implementing a ServletFilter to kind of hide preflight requests from Spring by wrapping the respective request and disguising it as a simple GET to avoid any of Spring Web's logic to trigger.
Afterwards I unwrap the request in the respective Controller...

`web.xml`:
```xml
  <filter> 
    <filter-name>hideOptionRequestFromSpringFilter</filter-name>
    <filter-class>HideOptionRequestFromSpringFilter</filter-class> 
  </filter> 
  <filter-mapping> 
    <filter-name>hideOptionRequestFromSpringFilter</filter-name>
    <url-pattern>/rest/*</url-pattern> 
  </filter-mapping>

HideOptionRequestFromSpringFilter.java:

import java.io.IOException;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;


/**
 * Hides the Origin header from Spring to skip its CORS processing.
 */
public class HideOptionRequestFromSpringFilter implements Filter {
    public final static String HIDE_ORIGIN_FROM_SPRING_ATTRIBUTE = "xxx.SkipSpringCorsProcessorFilter.Origin";
    public final static String HIDE_METHOD_FROM_SPRING_ATTRIBUTE = "xxx.SkipSpringCorsProcessorFilter.Method";

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        UserBean.detachFromCurrentThread();

        final HttpServletRequest httpRequest = (HttpServletRequest) request;

        if (HttpMethod.OPTIONS.matches(httpRequest.getMethod())) {
            httpRequest.setAttribute(HIDE_ORIGIN_FROM_SPRING_ATTRIBUTE, httpRequest.getHeader(HttpHeaders.ORIGIN));
            httpRequest.setAttribute(HIDE_METHOD_FROM_SPRING_ATTRIBUTE, httpRequest.getMethod());

            HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(httpRequest) {
                @Override
                public String getHeader(String name) {
                    if (HttpHeaders.ORIGIN.equalsIgnoreCase(name)) {
                        return null;
                    }
                    return super.getHeader(name);
                }

                @Override
                public String getMethod() {
                    return "GET";
                }
            };

            chain.doFilter(wrapper, response);
        } else {
            chain.doFilter(request, response);
        }
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

    @Override
    public void destroy() {}
}

RestService.java:

@RestController
public class RestService {
    /**
     * Handle multi-part requests.
     */
    @RequestMapping(path = "/rest/**")
    protected void service(
        HttpServletRequest request, 
        HttpServletResponse response
    ) throws ServletException, IOException {
        // Unwrap original Request; Counterpart to HideOptionRequestFromSpringFilter
        if (request instanceof HttpServletRequestWrapper) {
            final HttpServletRequestWrapper w = (HttpServletRequestWrapper) request;
            final HttpServletRequest wrappedRequest = (HttpServletRequest) w.getRequest();

            if (HttpMethod.GET.matches(request.getMethod()) && HttpMethod.OPTIONS.matches(wrappedRequest.getMethod())) {
                request = wrappedRequest;
            }
        }

        // Handle request in our very own way...
    }

Maybe in a next step we'll try to adopt Spring Web's handling of preflight requests into our custom (and scriptable) way of handling them. Won't be an easy task, thou.

Comment From: sdeleuze

Maybe rejecting preflight requests when there is no CORS config is indeed too strong, as the lack of proper response header could be enough to get the right default behavior, and it could improve consistency between preflight/non-preflight request and help non-builtin CORS support like the use cases described above. I will tentatively see if I can provide a reasonable solution for 6.2.x (otherwise, I may target 7.0.x).

Comment From: sdeleuze

After discussing with @rstoyanchev, we decided to target Spring Framework 7.0 as there is a workaround and we prefer to get early feedback on this change, even if in theory that should be fine.

Comment From: joelmarty

@sdeleuze I believe the PRs submitted as fixes for this issue are not enough to fix the issue as described by @kenips (at least in the reactive version): the code for AbstractHandlerMapping.getHandler() is:

    @Override
    public Mono<Object> getHandler(ServerWebExchange exchange) {
        initApiVersion(exchange);
        return getHandlerInternal(exchange).map(handler -> {
            if (logger.isDebugEnabled()) {
                logger.debug(exchange.getLogPrefix() + "Mapped to " + handler);
            }
            ServerHttpRequest request = exchange.getRequest();
            if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
                CorsConfiguration config = (this.corsConfigurationSource != null ?
                        this.corsConfigurationSource.getCorsConfiguration(exchange) : null);
                CorsConfiguration handlerConfig = getCorsConfiguration(handler, exchange);
                config = (config != null ? config.combine(handlerConfig) : handlerConfig);
                if (config != null) {
                    config.validateAllowCredentials();
                    config.validateAllowPrivateNetwork();
                }
                if (!this.corsProcessor.process(config, exchange) || CorsUtils.isPreFlightRequest(request)) {
                    return NO_OP_HANDLER;
                }
            }
            if (getApiVersionStrategy() != null) {
                Comparable<?> version = exchange.getAttribute(API_VERSION_ATTRIBUTE);
                if (version != null) {
                    getApiVersionStrategy().handleDeprecations(version, handler, exchange);
                }
            }
            return handler;
        });
    }

If the request is a CORS preflight request, the NO_OP_HANDLER is always returned. In the case of Spring Cloud Gateway, this prevents the request to be forwarded to the gateway's backends because the exchange is still short-circuited here.

I don't know what a good solution would look like, but it looks to me like the absence of CORS configuration could signal that no CORS handling should be done at all, and that the handler returned by getHandlerInternal() should be returned.

Comment From: sdeleuze

@joelmarty Please create a dedicated new issue with a reproducer (standalone minimal Boot application to reproduced, attached to the issue, or a link to the Git repository) using Spring Boot 4.0-0-RC2, with reproduction instructions and a description explaining what behavior you see and what behavior you would like to happen instead.

Comment From: joelmarty

@sdeleuze Opened https://github.com/spring-projects/spring-framework/issues/35789, with link to repoducer repo