Skip to content

CORS configuration is based on outdated RouteDefinitions when using Redis and RefreshRoutesEvent #3774

Closed
@PeterMue

Description

@PeterMue

 Describe the bug
When using the RedisRouteDefinitionRepository and the actuator endpoint /gateway/refresh to refresh the routes, the per-route CORS configuration is done before the actual reload of the routes from the redis completes and thus the CORS configuration is based on the previous state of the route.

This happens due to the asynchronous behaviour of the lettuce driver that loads the RouteDefintions from Redis.

The timeline is as follows:

  1. POST to the Actuator /gateway/refresh Endpoint emits a RefreshRoutesEvent
  2. Method org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent() is processed
    (which contains allRoutes.subscribe(list -> updateCache(Flux.fromIterable(list)), this::handleRefreshError);)
  3. Method org.springframework.cloud.gateway.filter.cors.CorsGatewayFilterApplicationListener.onApplicationEvent() is
    processed
  4. The subscribe callback from 2. is executed which updates the cache

Setting some Breakpoints that just log the current Thread reveals exactly this sequence:

Method 'org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent()' entered at org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent(CachingRouteLocator.java:87)
reactor-http-nio-2 -> CachingRouteLocator
Method 'org.springframework.cloud.gateway.filter.cors.CorsGatewayFilterApplicationListener.onApplicationEvent()' entered at org.springframework.cloud.gateway.filter.cors.CorsGatewayFilterApplicationListener.onApplicationEvent(CorsGatewayFilterApplicationListener.java:65)
reactor-http-nio-2 -> CorsGatewayFilterApplicationListener
Breakpoint reached at org.springframework.cloud.gateway.route.CachingRouteLocator.lambda$onApplicationEvent$2(CachingRouteLocator.java:98)
lettuce-nioEventLoop-5-1 -> CachingRouteLocator

As the fetch from redis runs async inside the lettuce EventLoop, there is no guarantee of the correct order of execution.

Versions

  • org.springframework.boot:spring-boot-starter-parent:3.4.4
  • org.springframework.boot:spring-boot-starter-data-redis-reactive:3.4.4
  • org.springframework.boot:spring-boot-starter-actuator:3.4.4
  • org.springframework.cloud:spring-cloud-dependencies:2024.0.1
  • org.springframework.cloud:spring-cloud-starter-gateway:4.2.1

Sample

  1. Download https://start.spring.io/#!type=maven-project&language=java&platformVersion=3.4.4&packaging=jar&jvmVersion=17&groupId=de.nuernberger.apigw.cors&artifactId=cors-sample&name=cors-sample&description=CORS%20Refresh%20Error%20Sample&packageName=de.nuernberger.apigw.cors.cors-sample&dependencies=cloud-gateway-reactive,actuator
  2. Configure application.properties
    spring.application.name=cors-sample
    management.endpoint.gateway.access=unrestricted
    management.endpoints.web.exposure.include=gateway
    spring.cloud.gateway.redis-route-definition-repository.enabled=true
    
  3. Create a Route via /actuator/gateway/routes/cors-sample
    POST http://localhost:8080/actuator/gateway/routes/cors-sample
    Content-Type: application/json
    
    {
       "id": "cors-sample",
       "uri": "http://localhost:9000/health",
       "predicates": [{
          "name": "Path",
          "args": {
             "patterns": "/cors-sample"
          }
       }],
       "filters": [],
       "metadata": {
          "cors": {
             "allowCredentials": false,
             "allowedOrigins": "*",
             "allowedMethods": [
                "GET",
                "POST"
             ],
             "allowedHeaders": "*",
             "maxAge": 1
          }
       }
    }
    
  4. Restart Application
  5. Verify Route is loaded via GET http://localhost:8080/actuator/gateway/routes/cors-sample
  6. Verify that a CORS Preflight request to /cors-sample returns the correct CORS headers
    OPTIONS http://localhost:8080/cors-sample
    Origin: http://localhost:8080
    Access-Control-Request-Method: GET
    
    should always return a 200 regardless of the origin
  7. Modify CORS configuration: set an explicit origin
    POST http://localhost:8080/actuator/gateway/routes/cors-sample
    Content-Type: application/json
    
    {
       "id": "cors-sample",
       "uri": "http://localhost:9000/health",
       "predicates": [{
          "name": "Path",
          "args": {
             "patterns": "/cors-sample"
          }
       }],
       "filters": [],
       "metadata": {
          "cors": {
             "allowCredentials": false,
             "allowedOrigins": "http://foobar",
             "allowedMethods": [
                "GET",
                "POST"
             ],
             "allowedHeaders": "*",
             "maxAge": 1
          }
       }
    } 
    
  8. Refresh via the actuator endpoint /actuator/gateway/refresh
    POST http://localhost:8080/actuator/gateway/refresh
    
  9. Verify that the route itself is returned correctly via
    GET http://localhost:8080/actuator/gateway/routes/cors-sample
    
    this should show the changed origin
  10. Although it seems correct, the internal state of the CORS configuration is still the old one. Which can be verified by sending a CORS preflight request to /cors-sample with the old origin
    OPTIONS http://localhost:8080/cors-sample
    Origin: http://localhost:8080
    Access-Control-Request-Method: GET
    
    this should return a 403 as the origin is not allowed anymore but returns a 200 and the old value for Access-Control-Allow-Origin: *

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Status

    No status

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions