WebTestClient - CORS with Spring Boot and Webflux

Viewed 2234

I have Vuejs frontend and a Spring Boot Webflux controller. Now the browser is complaining with CORS when doing a call to Spring Boot.

Access to XMLHttpRequest at 'https://...' from origin 'https://...' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

To enable CORS I tried to use the @CrossOrigin annotation as described here: https://www.baeldung.com/spring-webflux-cors

But it didn't help and somehow Spring does not send the CORS headers back in the response even with the @CrossOrigin annotation in the controller.

I also tried with the WebTestClient as described in the Bealdung tutorial and it confirms the header is not there:

java.lang.AssertionError: Response header 'Access-Control-Allow-Origin' expected:<[*]> but was:<null>

> OPTIONS /cors-enabled-endpoint
> WebTestClient-Request-Id: [1]
> Origin: [http://any-origin.com]
> Access-Control-Request-Method: [GET]

No content

< 500 INTERNAL_SERVER_ERROR Internal Server Error
< Vary: [Origin, Access-Control-Request-Method, Access-Control-Request-Headers]

0 bytes of content (unknown content-type).

If I test with response.expectHeader().exists("Access-Control-Allow-Origin"); I get:

java.lang.AssertionError: Response header 'Access-Control-Allow-Origin' does not exist

Any Idea why CORS configuration as described in the link above does not work? I also tried Enabling CORS on the Global Configuration and Enabling CORS with a WebFilter. But nothing seems to work.

5 Answers

Thinice's solution is 99.9% there. The Spring WebTestClient CORS testing requires that the URI contain any hostname and port. The actual hostname and port is ignored by WebTestClient to connect to the test web server:

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
class MyTestClass {

    @Autowired
    private WebTestClient webTestClient;
    
    @Test
    void corsTest() {
        webTestClient.get()
                .uri("http://hostname-ignored:666/cors-enabled-endpoint")
                .header("Origin", "http://any-origin.com")
                .exchange()   
                .expectHeader()
                  .valueEquals("Access-Control-Allow-Origin", "*");
    }    
}

Furthermore, one can omit the explicit port and it will continue to work:

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
class MyTestClass {

    @Autowired
    private WebTestClient webTestClient;
    
    @Test
    void corsTest() {
        webTestClient.get()
                .uri("http://look-ma-no-port/cors-enabled-endpoint")
                .header("Origin", "http://any-origin.com")
                .exchange()   
                .expectHeader()
                  .valueEquals("Access-Control-Allow-Origin", "*");
    }
}

To enable CORS for all endpoints, one can use the given WebFluxConfigurer:

@Configuration(proxyBeanMethods = false)
public class MyWebFluxConfigurer {

  @Bean
  public WebFluxConfigurer corsConfigurer() {
    return new WebFluxConfigurerComposite() {

      @Override
      public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**").allowedOrigins("*").allowedMethods("*");
      }
    };
  }
}
  • Including the Origin header is important. Without the Origin header the Access-Control-Allow-Origin header will not be set and the test will fail.

  • Setting Origin as the same hostname as the URI will be considered the same origin, so no cross origin headers are set.

  • RichArt's suggestion to include the spring-boot-starter-web dependency is not specifically required to enable CORS testing.

  • This answer only applies to testing. It assumes you have enabled CORS in your server.

  • My answer is using Spring WebFlux 5.3.10.

For me what actually worked without adding the spring-starter-web dependency was to use a web filter and force return if the http method is OPTIONS.

@Configuration
public class CorsGlobalConfiguration implements WebFilter {


@Override
public Mono<Void> filter(ServerWebExchange serverWebExchange,
                         WebFilterChain webFilterChain) {
    ServerHttpRequest request = serverWebExchange.getRequest();
    ServerHttpResponse response = serverWebExchange.getResponse();
    HttpHeaders headers = response.getHeaders();
    headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, "*");
    headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "POST, GET, PUT, OPTIONS, DELETE, PATCH");
    headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
    headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "*");
    headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "*");
    headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "18000L");
    if (request.getMethod() == HttpMethod.OPTIONS) {
        response.setStatusCode(HttpStatus.OK);
        return Mono.empty();//HERE
    }
    return webFilterChain.filter(serverWebExchange);
}


}

Source

@RichArt's solution is 99% close - but you don't need the dependency (spring-boot-starter-web); since it's checking CORS, it appears Spring needs help knowing what scheme and host you're hitting it from in the test; it can't derive the 'scheme' (http) or the host with the shorter URI.

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
class MyTestClass {

    @Autowired
    private WebTestClient webTestClient;

    @LocalServerPort
    private Int lsp;

    @Test
    void corsTest() {
        ResponseSpec response = webTestClient.get()
                .uri("http://localhost" + lsp.toString() + "/cors-enabled-endpoint")
                .header("Origin", "http://any-origin.com")
                .exchange();

        response.expectHeader()
                .valueEquals("Access-Control-Allow-Origin", "*");
    }

}

In the JavaDocs of the @CrossOrigin there is mentioning

Both Spring Web MVC and Spring WebFlux support this annotation through the RequestMappingHandlerMapping in their respective modules. The values from each type and method level pair of annotations are added to a CorsConfiguration

Which means your CORS settings will be added to CorsConfiguration

If you are using Spring Security you need to enable CORS so that your CorsConfiguration will be used. See below a small example

   protected void configure(HttpSecurity http) throws Exception {
        http
            .cors()
            ...
    }

From the JavaDocs

Adds a CorsFilter to be used. If a bean by the name of corsFilter is provided, that CorsFilter is used. Else if corsConfigurationSource is defined, then that CorsConfiguration is used. Otherwise, if Spring MVC is on the classpath a HandlerMappingIntrospector is used.

Note, if you are sending credentials with the request (cookies) you need to add it to your CORS settings

@CrossOrigin(allowCredentials = "true")

The solution was to use following configuration for the WebTestClient:

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient

class MyTestClass {

    @Autowired
    private WebTestClient webTestClient;

    @Test
    void corsTest() {
        ResponseSpec response = webTestClient.get()
                .uri("/cors-enabled-endpoint")
                .header("Origin", "http://any-origin.com")
                .exchange();

        response.expectHeader()
                .valueEquals("Access-Control-Allow-Origin", "*");
    }

}

And I also needed to add the spring-boot-starter-web dependency, which is very strange because I am using only Webflux. But without it the test still fails with IllegalArgumentException: Actual request scheme must not be null.

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
</dependency>

Now, the test is green!

Related