Closed
Description
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:
- POST to the Actuator
/gateway/refresh
Endpoint emits aRefreshRoutesEvent
- Method
org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent()
is processed
(which containsallRoutes.subscribe(list -> updateCache(Flux.fromIterable(list)), this::handleRefreshError);
) - Method
org.springframework.cloud.gateway.filter.cors.CorsGatewayFilterApplicationListener.onApplicationEvent()
is
processed - 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
- 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
- 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
- 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 } } }
- Restart Application
- Verify Route is loaded via
GET http://localhost:8080/actuator/gateway/routes/cors-sample
- Verify that a CORS Preflight request to
/cors-sample
returns the correct CORS headersshould always return a 200 regardless of the originOPTIONS http://localhost:8080/cors-sample Origin: http://localhost:8080 Access-Control-Request-Method: GET
- 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 } } }
- Refresh via the actuator endpoint
/actuator/gateway/refresh
POST http://localhost:8080/actuator/gateway/refresh
- Verify that the route itself is returned correctly via
this should show the changed origin
GET http://localhost:8080/actuator/gateway/routes/cors-sample
- 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 originthis should return a 403 as the origin is not allowed anymore but returns a 200 and the old value forOPTIONS http://localhost:8080/cors-sample Origin: http://localhost:8080 Access-Control-Request-Method: GET
Access-Control-Allow-Origin: *