This is an automated email from the ASF dual-hosted git repository.
Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new e22a6d9be4 perf(web): reuse compiled CORS origin patterns (#7263)
e22a6d9be4 is described below
commit e22a6d9be4b324c1f79f7ef1c2136d715c65be1f
Author: Liming Deng <[email protected]>
AuthorDate: Sat Oct 17 20:48:32 2026 +0800
perf(web): reuse compiled CORS origin patterns (#7263)
* perf(web): reuse compiled CORS origin patterns
* docs(cors): document and test invalid origin patterns at startup
---
RELEASE-NOTES.md | 4 +++
.../org/apache/shenyu/web/filter/CrossFilter.java | 16 +++++++++-
.../apache/shenyu/web/filter/CrossFilterTest.java | 36 ++++++++++++++++++++++
3 files changed, 55 insertions(+), 1 deletion(-)
diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md
index 4ad0c71297..d79cb9ef3b 100644
--- a/RELEASE-NOTES.md
+++ b/RELEASE-NOTES.md
@@ -28,6 +28,10 @@
### Behavior Changes
+- CORS origin patterns are compiled when the gateway starts. An invalid
+ `shenyu.cross.allowed-origin.origin-regex` now fails startup with
+ `PatternSyntaxException`, rather than failing the first matching request.
+ Validate the expression before deploying a configuration change.
- HTTP client registration now treats non-success Admin responses as failures,
using the same bounded failback policy as transport errors (18 retry attempts
per pending task). The text response contract does not reliably distinguish
diff --git
a/shenyu-web/src/main/java/org/apache/shenyu/web/filter/CrossFilter.java
b/shenyu-web/src/main/java/org/apache/shenyu/web/filter/CrossFilter.java
index 0aa4c6b074..1edec14fb3 100644
--- a/shenyu-web/src/main/java/org/apache/shenyu/web/filter/CrossFilter.java
+++ b/shenyu-web/src/main/java/org/apache/shenyu/web/filter/CrossFilter.java
@@ -50,8 +50,13 @@ public class CrossFilter implements WebFilter {
private final CrossFilterConfig filterConfig;
+ private volatile Pattern originPattern;
+
public CrossFilter(final CrossFilterConfig filterConfig) {
this.filterConfig = filterConfig;
+ if (Objects.nonNull(filterConfig.getAllowedOrigin()) &&
StringUtils.isNotBlank(filterConfig.getAllowedOrigin().getOriginRegex())) {
+ originPattern =
Pattern.compile(filterConfig.getAllowedOrigin().getOriginRegex().trim());
+ }
}
@Override
@@ -91,7 +96,7 @@ public class CrossFilter implements WebFilter {
// if the origin is not allow check match origin again
String originRegex;
if (!allowCors && StringUtils.isNotBlank(originRegex =
this.filterConfig.getAllowedOrigin().getOriginRegex())) {
- allowCors = Pattern.matches(originRegex.trim(), origin);
+ allowCors =
getOriginPattern(originRegex.trim()).matcher(origin).matches();
}
}
if (allowCors) {
@@ -121,6 +126,15 @@ public class CrossFilter implements WebFilter {
return chain.filter(exchange);
}
+ private Pattern getOriginPattern(final String regex) {
+ Pattern pattern = originPattern;
+ if (Objects.isNull(pattern) || !pattern.pattern().equals(regex)) {
+ pattern = Pattern.compile(regex);
+ originPattern = pattern;
+ }
+ return pattern;
+ }
+
/**
* Filter the same headers.
*
diff --git
a/shenyu-web/src/test/java/org/apache/shenyu/web/filter/CrossFilterTest.java
b/shenyu-web/src/test/java/org/apache/shenyu/web/filter/CrossFilterTest.java
index 68172c0b1d..79a74a15ca 100644
--- a/shenyu-web/src/test/java/org/apache/shenyu/web/filter/CrossFilterTest.java
+++ b/shenyu-web/src/test/java/org/apache/shenyu/web/filter/CrossFilterTest.java
@@ -25,6 +25,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
+import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
@@ -200,4 +201,39 @@ public final class CrossFilterTest {
Assertions.assertTrue(Pattern.matches(regex, "http://abc.com"));
Assertions.assertFalse(Pattern.matches(regex, "http://aabc.com"));
}
+
+ @Test
+ public void testInvalidInitialOriginPatternFailsDuringConstruction() {
+ CrossFilterConfig config = new CrossFilterConfig();
+ config.getAllowedOrigin().setOriginRegex("[");
+ Assertions.assertThrows(java.util.regex.PatternSyntaxException.class,
() -> new CrossFilter(config));
+ }
+
+ @Test
+ void reusesCompiledPatternAndObservesConfigurationChanges() {
+ CrossFilterConfig config = new CrossFilterConfig();
+ config.getAllowedOrigin().setOriginRegex(" https://allowed[.]example
");
+ CrossFilter filter = new CrossFilter(config);
+ Object pattern = ReflectionTestUtils.getField(filter, "originPattern");
+ Assertions.assertNotNull(pattern);
+ for (int i = 0; i < 3; i++) {
+ assertOrigin(filter, "https://allowed.example", true);
+ assertOrigin(filter, "https://other.example", false);
+ Assertions.assertSame(pattern,
ReflectionTestUtils.getField(filter, "originPattern"));
+ }
+ config.getAllowedOrigin().setOriginRegex("https://other[.]example");
+ assertOrigin(filter, "https://other.example", true);
+ assertOrigin(filter, "https://allowed.example", false);
+ Assertions.assertNotSame(pattern, ReflectionTestUtils.getField(filter,
"originPattern"));
+ config.getAllowedOrigin().setOriginRegex(" ");
+ assertOrigin(filter, "https://other.example", false);
+ config.setAllowedOrigin(null);
+ assertOrigin(filter, "https://other.example", false);
+ }
+
+ private void assertOrigin(final CrossFilter filter, final String origin,
final boolean allowed) {
+ ServerWebExchange exchange =
MockServerWebExchange.from(MockServerHttpRequest.get("http://localhost").header(HttpHeaders.ORIGIN,
origin));
+ StepVerifier.create(filter.filter(exchange, ignored ->
Mono.empty())).verifyComplete();
+ Assertions.assertEquals(allowed ? origin : null,
exchange.getResponse().getHeaders().getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN));
+ }
}