Skip to content

Commit 2b8b806

Browse files
committed
Use LinkedHashMap for CORS configurations in CorsGatewayFilterApplicationListener to preserve insertion order. Fixes GH-3805.
Signed-off-by: Yavor Chamov <[email protected]>
1 parent ff35c8a commit 2b8b806

File tree

2 files changed

+67
-30
lines changed

2 files changed

+67
-30
lines changed

spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/cors/CorsGatewayFilterApplicationListener.java

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,34 @@
3434
import org.springframework.web.cors.CorsConfiguration;
3535

3636
/**
37-
* This class updates Cors configuration each time a {@link RefreshRoutesResultEvent} is
38-
* consumed. The {@link Route}'s predicates are inspected for a
39-
* {@link PathRoutePredicateFactory} and the first pattern is used.
37+
* <p>
38+
* For each {@link Route}, this listener inspects its predicates and looks for an instance
39+
* of {@link PathRoutePredicateFactory}. If a path predicate is found, the first defined
40+
* path pattern is extracted and used as the key for associating the route-specific
41+
* {@link CorsConfiguration}.
42+
* </p>
43+
*
44+
* <p>
45+
* After collecting all route-level CORS configurations, the listener merges them with
46+
* globally defined configurations from {@link GlobalCorsProperties}, ensuring that
47+
* route-specific configurations take precedence over global ones in case of conflicts
48+
* (e.g., both defining CORS rules for {@code /**}).
49+
* </p>
50+
*
51+
* <p>
52+
* The merged configuration map is then applied to the
53+
* {@link RoutePredicateHandlerMapping} via {@code setCorsConfigurations}.
54+
* </p>
55+
*
56+
* <p>
57+
* Note: A {@link LinkedHashMap} is used to store the merged configurations to preserve
58+
* insertion order, which ensures predictable CORS resolution when multiple path patterns
59+
* could match a request.
60+
* </p>
4061
*
4162
* @author Fredrich Ombico
4263
* @author Abel Salgado Romero
64+
* @author Yavor Chamov
4365
*/
4466
public class CorsGatewayFilterApplicationListener implements ApplicationListener<RefreshRoutesResultEvent> {
4567

@@ -69,12 +91,16 @@ public void onApplicationEvent(RefreshRoutesResultEvent event) {
6991
routes.forEach(route -> {
7092
Optional<CorsConfiguration> corsConfiguration = getCorsConfiguration(route);
7193
corsConfiguration.ifPresent(configuration -> {
72-
var pathPredicate = getPathPredicate(route);
94+
String pathPredicate = getPathPredicate(route);
7395
corsConfigurations.put(pathPredicate, configuration);
7496
});
7597
});
7698

77-
corsConfigurations.putAll(globalCorsProperties.getCorsConfigurations());
99+
globalCorsProperties.getCorsConfigurations().forEach((path, config) -> {
100+
if (!corsConfigurations.containsKey(path)) {
101+
corsConfigurations.put(path, config);
102+
}
103+
});
78104
routePredicateHandlerMapping.setCorsConfigurations(corsConfigurations);
79105
});
80106
}

spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/cors/CorsGatewayFilterApplicationListenerTests.java

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,21 @@
4646
/**
4747
* Tests for {@link CorsGatewayFilterApplicationListener}.
4848
*
49-
* <p>This test verifies that the merged CORS configurations - composed of per-route metadata
50-
* and at the global level - maintain insertion order, as defined by the use of {@link LinkedHashMap}.
51-
* Preserving insertion order helps for predictable and deterministic CORS behavior
52-
* when resolving multiple matching path patterns.
49+
* <p>
50+
* This test verifies that the merged CORS configurations - composed of per-route metadata
51+
* and at the global level - maintain insertion order, as defined by the use of
52+
* {@link LinkedHashMap}. Preserving insertion order helps for predictable and
53+
* deterministic CORS behavior when resolving multiple matching path patterns.
5354
* </p>
5455
*
55-
* <p>The test builds actual {@link Route} instances with {@code Path} predicates and verifies
56-
* that the resulting configuration map passed to {@link RoutePredicateHandlerMapping#setCorsConfigurations(Map)}
57-
* respects the declared order of:
56+
* <p>
57+
* The test builds actual {@link Route} instances with {@code Path} predicates and
58+
* verifies that the resulting configuration map passed to
59+
* {@link RoutePredicateHandlerMapping#setCorsConfigurations(Map)} respects the declared
60+
* order of:
5861
* <ul>
59-
* <li>Route-specific CORS configurations (in the order the routes are discovered)</li>
60-
* <li>Global CORS configurations (in insertion order)</li>
62+
* <li>Route-specific CORS configurations (in the order the routes are discovered)</li>
63+
* <li>Global CORS configurations (in insertion order)</li>
6164
* </ul>
6265
* </p>
6366
*
@@ -67,17 +70,29 @@
6770
class CorsGatewayFilterApplicationListenerTests {
6871

6972
private static final String GLOBAL_PATH_1 = "/global1";
73+
7074
private static final String GLOBAL_PATH_2 = "/global2";
75+
7176
private static final String ROUTE_PATH_1 = "/route1";
77+
7278
private static final String ROUTE_PATH_2 = "/route2";
79+
7380
private static final String ORIGIN_GLOBAL_1 = "https://global1.com";
81+
7482
private static final String ORIGIN_GLOBAL_2 = "https://global2.com";
83+
7584
private static final String ORIGIN_ROUTE_1 = "https://route1.com";
85+
7686
private static final String ORIGIN_ROUTE_2 = "https://route2.com";
87+
7788
private static final String ROUTE_ID_1 = "route1";
89+
7890
private static final String ROUTE_ID_2 = "route2";
91+
7992
private static final String ROUTE_URI = "https://spring.io";
93+
8094
private static final String METADATA_KEY = "cors";
95+
8196
private static final String ALLOWED_ORIGINS_KEY = "allowedOrigins";
8297

8398
@Mock
@@ -96,8 +111,7 @@ class CorsGatewayFilterApplicationListenerTests {
96111
@BeforeEach
97112
void setUp() {
98113
globalCorsProperties = new GlobalCorsProperties();
99-
listener = new CorsGatewayFilterApplicationListener(globalCorsProperties,
100-
handlerMapping, routeLocator);
114+
listener = new CorsGatewayFilterApplicationListener(globalCorsProperties, handlerMapping, routeLocator);
101115
}
102116

103117
@Test
@@ -118,16 +132,14 @@ void testOnApplicationEvent_preservesInsertionOrder_withRealRoutes() {
118132
verify(handlerMapping).setCorsConfigurations(corsConfigurations.capture());
119133

120134
Map<String, CorsConfiguration> mergedCorsConfigurations = corsConfigurations.getValue();
121-
assertThat(mergedCorsConfigurations.keySet())
122-
.containsExactly(ROUTE_PATH_1, ROUTE_PATH_2, GLOBAL_PATH_1, GLOBAL_PATH_2);
135+
assertThat(mergedCorsConfigurations.keySet()).containsExactly(ROUTE_PATH_1, ROUTE_PATH_2, GLOBAL_PATH_1,
136+
GLOBAL_PATH_2);
123137
assertThat(mergedCorsConfigurations.get(GLOBAL_PATH_1).getAllowedOrigins())
124-
.containsExactly(ORIGIN_GLOBAL_1);
138+
.containsExactly(ORIGIN_GLOBAL_1);
125139
assertThat(mergedCorsConfigurations.get(GLOBAL_PATH_2).getAllowedOrigins())
126-
.containsExactly(ORIGIN_GLOBAL_2);
127-
assertThat(mergedCorsConfigurations.get(ROUTE_PATH_1).getAllowedOrigins())
128-
.containsExactly(ORIGIN_ROUTE_1);
129-
assertThat(mergedCorsConfigurations.get(ROUTE_PATH_2).getAllowedOrigins())
130-
.containsExactly(ORIGIN_ROUTE_2);
140+
.containsExactly(ORIGIN_GLOBAL_2);
141+
assertThat(mergedCorsConfigurations.get(ROUTE_PATH_1).getAllowedOrigins()).containsExactly(ORIGIN_ROUTE_1);
142+
assertThat(mergedCorsConfigurations.get(ROUTE_PATH_2).getAllowedOrigins()).containsExactly(ORIGIN_ROUTE_2);
131143
});
132144
}
133145

@@ -141,12 +153,11 @@ private CorsConfiguration createCorsConfig(String origin) {
141153
private Route buildRoute(String id, String path, String allowedOrigin) {
142154

143155
return Route.async()
144-
.id(id)
145-
.uri(ROUTE_URI)
146-
.predicate(new PathRoutePredicateFactory()
147-
.apply(config -> config.setPatterns(List.of(path))))
148-
.metadata(METADATA_KEY, Map.of(ALLOWED_ORIGINS_KEY, List.of(allowedOrigin)))
149-
.build();
156+
.id(id)
157+
.uri(ROUTE_URI)
158+
.predicate(new PathRoutePredicateFactory().apply(config -> config.setPatterns(List.of(path))))
159+
.metadata(METADATA_KEY, Map.of(ALLOWED_ORIGINS_KEY, List.of(allowedOrigin)))
160+
.build();
150161
}
151162

152163
}

0 commit comments

Comments
 (0)