From 0a38fa7ada0e71b0f0179a806445e44412fc5661 Mon Sep 17 00:00:00 2001 From: Tomas Hofman Date: Wed, 1 Oct 2025 16:27:40 +0200 Subject: [PATCH 1/2] Improve path matching in RESTEasy Reactive for ambiguous path templates The PathMatcher now returns list of all possible matches rather than just the first match. This is for case when the first match end up not matching the request path, the remaining, lower priority matches, can be tried. --- .../reactive/server/mapping/PathMatcher.java | 14 +++-- .../server/mapping/RequestMapper.java | 12 +++-- .../test/matching/PathParamOverlapTest.java | 51 +++++++++++++++++++ 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/mapping/PathMatcher.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/mapping/PathMatcher.java index d13d30d0fddbe..bb94ca823e1a7 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/mapping/PathMatcher.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/mapping/PathMatcher.java @@ -1,5 +1,7 @@ package org.jboss.resteasy.reactive.server.mapping; +import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Set; @@ -34,24 +36,28 @@ class PathMatcher implements Dumpable { * @param path The relative path to match * @return The match match. This will never be null, however if none matched its value field will be */ - PathMatch match(String path) { + List> match(String path) { int length = path.length(); final int[] lengths = this.lengths; + ArrayList> matches = new ArrayList<>(); for (int i = 0; i < lengths.length; ++i) { int pathLength = lengths[i]; if (pathLength == length) { SubstringMap.SubstringMatch next = paths.get(path, length); if (next != null) { - return new PathMatch<>(path, "", next.getValue()); + matches.add(new PathMatch<>(path, "", next.getValue())); } } else if (pathLength < length) { SubstringMap.SubstringMatch next = paths.get(path, pathLength); if (next != null) { - return new PathMatch<>(next.getKey(), path.substring(pathLength), next.getValue()); + matches.add(new PathMatch<>(next.getKey(), path.substring(pathLength), next.getValue())); } } } - return defaultMatch(path); + if (!matches.isEmpty()) { + return matches; + } + return Collections.singletonList(defaultMatch(path)); } PathMatch defaultMatch(String path) { diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/mapping/RequestMapper.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/mapping/RequestMapper.java index e85367904451a..9ee30258c1c61 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/mapping/RequestMapper.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/mapping/RequestMapper.java @@ -4,6 +4,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.function.BiConsumer; import java.util.regex.Matcher; @@ -48,9 +49,12 @@ public void accept(String stem, ArrayList> list) { * @return best RequestMatch, or null if the path has no match */ public RequestMatch map(String path) { - var result = mapFromPathMatcher(path, requestPaths.match(path), 0); - if (result != null) { - return result; + List>>> matches = requestPaths.match(path); + for (PathMatcher.PathMatch>> match : matches) { + var result = mapFromPathMatcher(path, match, 0); + if (result != null) { + return result; + } } // the following code is meant to handle cases like https://github.com/quarkusio/quarkus/issues/30667 @@ -68,7 +72,7 @@ public RequestMatch continueMatching(String path, RequestMatch lastMatch) return null; } - var initialMatches = requestPaths.match(path); + var initialMatches = requestPaths.match(path).get(0); var result = mapFromPathMatcher(path, initialMatches, 0); if (result != null) { int idx = nextMatchStartingIndex(initialMatches, lastMatch); diff --git a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/PathParamOverlapTest.java b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/PathParamOverlapTest.java index 436f4d826c31e..d548766003e6b 100644 --- a/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/PathParamOverlapTest.java +++ b/independent-projects/resteasy-reactive/server/vertx/src/test/java/org/jboss/resteasy/reactive/server/vertx/test/matching/PathParamOverlapTest.java @@ -53,6 +53,44 @@ public void test() { get("/hello/other/test/wrong") .then() .statusCode(404); + + get("/hello/foo") + .then() + .statusCode(404); + + get("/hello/foo/value") + .then() + .statusCode(200) + .body(equalTo("Foo value")); + + get("/hello/foo/bar") + .then() + .statusCode(200) + .body(equalTo("Foo bar")); + + get("/hello/foo/bar/value") + .then() + .statusCode(200) + .body(equalTo("FooBar value")); + + get("/hello/foo/bah_value") + .then() + .statusCode(200) + .body(equalTo("Foo bah_value")); + + get("/hello/foo/bar_value") + .then() + .statusCode(200) + .body(equalTo("Foo bar_value")); + } + + // TODO: remove this test before commit + @Test + public void test2() { + get("/hello/foo/bar_value") + .then() + .statusCode(200) + .body(equalTo("Foo bar_value")); } @Path("/hello") @@ -70,5 +108,18 @@ public String test() { public String second(@RestPath String id) { return "Hello " + id; } + + @GET + @Path("/foo/{param}") + public String foo(@RestPath String param) { + return "Foo " + param; + } + + @GET + @Path("/foo/bar/{param}") + public String fooBar(@RestPath String param) { + return "FooBar " + param; + } + } } From 6a9fb11e875c0cec62f2f2cc393752398d291c4d Mon Sep 17 00:00:00 2001 From: Tomas Hofman Date: Tue, 7 Oct 2025 15:20:02 +0200 Subject: [PATCH 2/2] Improve path matching in RESTEasy Reactive for ambiguous path templates This solves further problems in case when the paths are spread over multiple resource classes. Improves earlier PR: https://github.com/quarkusio/quarkus/pull/47386 --- .../server/mapping/RequestMapper.java | 21 +++++++++++------- .../test/matching/PathParamOverlapTest.java | 22 +++++++++++++------ 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/mapping/RequestMapper.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/mapping/RequestMapper.java index 9ee30258c1c61..4462c019c5a16 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/mapping/RequestMapper.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/mapping/RequestMapper.java @@ -72,16 +72,21 @@ public RequestMatch continueMatching(String path, RequestMatch lastMatch) return null; } - var initialMatches = requestPaths.match(path).get(0); - var result = mapFromPathMatcher(path, initialMatches, 0); - if (result != null) { - int idx = nextMatchStartingIndex(initialMatches, lastMatch); - return mapFromPathMatcher(path, initialMatches, idx); + var initialMatchesList = requestPaths.match(path); + for (var initialMatches: initialMatchesList) { + var result = mapFromPathMatcher(path, initialMatches, 0); + if (result != null) { + int idx = nextMatchStartingIndex(initialMatches, lastMatch); + RequestMatch match = mapFromPathMatcher(path, initialMatches, idx); + if (match != null) { + return match; + } + } } // the following code is meant to handle cases like https://github.com/quarkusio/quarkus/issues/30667 - initialMatches = requestPaths.defaultMatch(path); - result = mapFromPathMatcher(path, initialMatches, 0); + var initialMatches = requestPaths.defaultMatch(path); + var result = mapFromPathMatcher(path, initialMatches, 0); if (result != null) { int idx = nextMatchStartingIndex(initialMatches, lastMatch); return mapFromPathMatcher(path, initialMatches, idx); @@ -105,7 +110,7 @@ private int nextMatchStartingIndex(PathMatcher.PathMatch