From 1a47eb4974d7a973117ce664089f9811513a7e5d Mon Sep 17 00:00:00 2001 From: Gio Gutierrez Date: Thu, 26 Feb 2026 15:07:26 -0500 Subject: [PATCH 1/3] fix(router): Traditional skipped match --- kong/router/traditional.lua | 57 +++++++++++++++++++++++++++++++-- spec/01-unit/08-router_spec.lua | 41 ++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 2 deletions(-) diff --git a/kong/router/traditional.lua b/kong/router/traditional.lua index 7f9bad76cdf..7499d51f58e 100644 --- a/kong/router/traditional.lua +++ b/kong/router/traditional.lua @@ -1135,6 +1135,53 @@ do [MATCH_RULES.URI] = function(category, ctx) -- no ctx.req_uri indexing since regex URIs have a higher priority than -- plain URIs + local matching = ctx.hits.matching_uri_values + if matching and next(matching) then + -- Union of routes for every path that matched the request URI, then + -- sort by regex_priority (desc) then created_at (asc) so more specific + -- routes are tried first. + local seen = {} + local seen_ref = {} + local list = {} + for path_value, _ in pairs(matching) do + local candidates = category.routes_by_uris[path_value] + if candidates then + for j = 1, candidates[0] do + local rt = candidates[j] + local id = rt.route and rt.route.id + if id then + if not seen[id] then + seen[id] = true + list[#list + 1] = rt + end + else + if not seen_ref[rt] then + seen_ref[rt] = true + list[#list + 1] = rt + end + end + end + end + end + if #list == 0 then + return nil + end + sort(list, function(a, b) + local rp1 = a.route and (a.route.regex_priority or 0) or 0 + local rp2 = b.route and (b.route.regex_priority or 0) or 0 + if rp1 ~= rp2 then + return rp1 > rp2 + end + local ca = a.route and a.route.created_at + local cb = b.route and b.route.created_at + if ca and cb then + return ca < cb + end + return false + end) + list[0] = #list + return list + end return category.routes_by_uris[ctx.hits.uri] end, @@ -1666,6 +1713,7 @@ function _M.new(routes, cache, cache_neg) -- uri match if match_regex_uris then + hits.matching_uri_values = nil for i = 1, regex_uris[0] do local from, _, err = re_find(req_uri, regex_uris[i].regex, "ajo") if err then @@ -1674,9 +1722,14 @@ function _M.new(routes, cache, cache_neg) end if from then - hits.uri = regex_uris[i].value + if not hits.matching_uri_values then + hits.matching_uri_values = {} + end + hits.matching_uri_values[regex_uris[i].value] = true + if not hits.uri then + hits.uri = regex_uris[i].value + end req_category = bor(req_category, MATCH_RULES.URI) - break end end end diff --git a/spec/01-unit/08-router_spec.lua b/spec/01-unit/08-router_spec.lua index 1b164904f4e..46b682bd9cf 100644 --- a/spec/01-unit/08-router_spec.lua +++ b/spec/01-unit/08-router_spec.lua @@ -1180,6 +1180,47 @@ for _, flavor in ipairs({ "traditional", "traditional_compatible", "expressions" end) end) + describe("multiple regex paths matching same URI and regex_priority", function() + it_trad_only("selects higher regex_priority route when multiple regex paths match and first match has headers", function() + local use_case = { + { + service = service, + route = { + id = "e8fb37f1-102d-461e-9c51-6608a6bb8181", + paths = { "~/api(/.*)" }, + headers = { ["X-API-Key"] = { "secret" } }, + regex_priority = 0, + }, + }, + { + service = service, + route = { + id = "e8fb37f1-102d-461e-9c51-6608a6bb8182", + paths = { "~/api/v1/login" }, + regex_priority = 200, + }, + }, + { + service = service, + route = { + id = "e8fb37f1-102d-461e-9c51-6608a6bb8183", + paths = { "~/api(/.*)" }, + regex_priority = 0, + }, + }, + } + + local router = assert(new_router(use_case)) + + local match_t = router:select("GET", "/api/v1/login", "example.com", "http", nil, nil, nil, nil, nil, {}) + assert.truthy(match_t) + assert.same(use_case[2].route, match_t.route) + if flavor == "traditional" then + assert.same("/api/v1/login", match_t.matches.uri) + end + end) + end) + describe("[wildcard host]", function() local use_case, router From bca3b55414eebe3e97060f656527551b0feb3772 Mon Sep 17 00:00:00 2001 From: Gio Gutierrez Date: Thu, 26 Feb 2026 15:59:58 -0500 Subject: [PATCH 2/3] chore: Add changelog file --- .../kong/fix-traditional-router-multiple-regex-paths.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog/unreleased/kong/fix-traditional-router-multiple-regex-paths.yml diff --git a/changelog/unreleased/kong/fix-traditional-router-multiple-regex-paths.yml b/changelog/unreleased/kong/fix-traditional-router-multiple-regex-paths.yml new file mode 100644 index 00000000000..2635fc8ab6b --- /dev/null +++ b/changelog/unreleased/kong/fix-traditional-router-multiple-regex-paths.yml @@ -0,0 +1,3 @@ +message: "Fixed traditional router to consider all matching path regexes for a request URI instead of only the first. When multiple path patterns matched the same URI, the router now unions candidates from every matching path and sorts by regex_priority (desc) and created_at (asc), so higher-priority routes (e.g. `/api/v1/login`) are no longer skipped in favor of a lower-priority fallback when an auth route's path matched first." +type: bugfix +scope: Core From 559f0aa9bb0d3db72cefee94e52ad261c120410b Mon Sep 17 00:00:00 2001 From: Gio Gutierrez Date: Tue, 10 Mar 2026 10:01:23 -0500 Subject: [PATCH 3/3] fix(router): Host regex matching --- kong/router/traditional.lua | 109 ++++++++++++++++++++++++++++++-- spec/01-unit/08-router_spec.lua | 69 ++++++++++++++++++++ 2 files changed, 173 insertions(+), 5 deletions(-) diff --git a/kong/router/traditional.lua b/kong/router/traditional.lua index 7499d51f58e..78273f67f56 100644 --- a/kong/router/traditional.lua +++ b/kong/router/traditional.lua @@ -1125,6 +1125,58 @@ end do local reducers = { [MATCH_RULES.HOST] = function(category, ctx) + local matching = ctx.hits.matching_host_values + if matching and next(matching) then + -- Union of routes for every host pattern that matched, then sort by + -- submatch_weight (desc, so port-specific wildcard wins), then + -- regex_priority (desc), then created_at (asc). + local seen = {} + local seen_ref = {} + local list = {} + for host_value, _ in pairs(matching) do + local candidates = category.routes_by_hosts[host_value] + if candidates then + for j = 1, candidates[0] do + local rt = candidates[j] + local id = rt.route and rt.route.id + if id then + if not seen[id] then + seen[id] = true + list[#list + 1] = rt + end + else + if not seen_ref[rt] then + seen_ref[rt] = true + list[#list + 1] = rt + end + end + end + end + end + if #list == 0 then + return nil + end + sort(list, function(a, b) + local sw1 = a.submatch_weight or 0 + local sw2 = b.submatch_weight or 0 + if sw1 ~= sw2 then + return sw1 > sw2 + end + local rp1 = a.route and (a.route.regex_priority or 0) or 0 + local rp2 = b.route and (b.route.regex_priority or 0) or 0 + if rp1 ~= rp2 then + return rp1 > rp2 + end + local ca = a.route and a.route.created_at + local cb = b.route and b.route.created_at + if ca and cb then + return ca < cb + end + return false + end) + list[0] = #list + return list + end return category.routes_by_hosts[ctx.hits.host or ctx.req_host] end, @@ -1426,7 +1478,11 @@ function _M.new(routes, cache, cache_neg) -- or IP ranges comparison functions local prefix_uris = { [0] = 0 } -- will be sorted by length local regex_uris = { [0] = 0 } + -- luacheck: ignore 311 + local num_distinct_regex_uris = 0 -- set in do block below, read in find_route closure local wildcard_hosts = { [0] = 0 } + -- luacheck: ignore 311 + local num_distinct_wildcard_hosts = 0 -- set in do block below, read in find_route closure local src_trust_funcs = { [0] = 0 } local dst_trust_funcs = { [0] = 0 } @@ -1509,6 +1565,34 @@ function _M.new(routes, cache, cache_neg) index_route_t(route_t, plain_indexes, prefix_uris, regex_uris, wildcard_hosts, src_trust_funcs, dst_trust_funcs) end + + -- Deduplicate regex_uris by path value so we run each regex at most once + -- per request, and know how many distinct patterns exist (for early exit). + local regex_uris_dedup = { [0] = 0 } + local seen_uri_value = {} + for i = 1, regex_uris[0] do + local uri_t = regex_uris[i] + if not seen_uri_value[uri_t.value] then + seen_uri_value[uri_t.value] = true + append(regex_uris_dedup, uri_t) + end + end + regex_uris = regex_uris_dedup + num_distinct_regex_uris = regex_uris[0] + + -- Deduplicate wildcard_hosts by host value so we run each regex at most once + -- per request, and know how many distinct patterns exist (for early exit). + local wildcard_hosts_dedup = { [0] = 0 } + local seen_host_value = {} + for i = 1, wildcard_hosts[0] do + local host_t = wildcard_hosts[i] + if not seen_host_value[host_t.value] then + seen_host_value[host_t.value] = true + append(wildcard_hosts_dedup, host_t) + end + end + wildcard_hosts = wildcard_hosts_dedup + num_distinct_wildcard_hosts = wildcard_hosts[0] end @@ -1702,9 +1786,19 @@ function _M.new(routes, cache, cache_neg) end if from then - hits.host = host.value + if num_distinct_wildcard_hosts > 1 then + if not hits.matching_host_values then + hits.matching_host_values = {} + end + hits.matching_host_values[host.value] = true + end + if not hits.host then + hits.host = host.value + end req_category = bor(req_category, MATCH_RULES.HOST) - break + if num_distinct_wildcard_hosts <= 1 then + break + end end end end @@ -1722,14 +1816,19 @@ function _M.new(routes, cache, cache_neg) end if from then - if not hits.matching_uri_values then - hits.matching_uri_values = {} + if num_distinct_regex_uris > 1 then + if not hits.matching_uri_values then + hits.matching_uri_values = {} + end + hits.matching_uri_values[regex_uris[i].value] = true end - hits.matching_uri_values[regex_uris[i].value] = true if not hits.uri then hits.uri = regex_uris[i].value end req_category = bor(req_category, MATCH_RULES.URI) + if num_distinct_regex_uris <= 1 then + break + end end end end diff --git a/spec/01-unit/08-router_spec.lua b/spec/01-unit/08-router_spec.lua index 46b682bd9cf..6b2e9788c6c 100644 --- a/spec/01-unit/08-router_spec.lua +++ b/spec/01-unit/08-router_spec.lua @@ -1221,6 +1221,75 @@ for _, flavor in ipairs({ "traditional", "traditional_compatible", "expressions" end) end) + describe("multiple wildcard hosts matching same host and regex_priority", function() + it_trad_only("selects higher regex_priority route when multiple wildcard hosts match", function() + local use_case = { + { + service = service, + route = { + id = "e8fb37f1-102d-461e-9c51-6608a6bb8191", + hosts = { "*.com" }, + regex_priority = 0, + }, + }, + { + service = service, + route = { + id = "e8fb37f1-102d-461e-9c51-6608a6bb8192", + hosts = { "*.example.com" }, + regex_priority = 200, + }, + }, + { + service = service, + route = { + id = "e8fb37f1-102d-461e-9c51-6608a6bb8193", + hosts = { "*.com" }, + regex_priority = 0, + }, + }, + } + + local router = assert(new_router(use_case)) + + -- api.example.com matches both *.com and *.example.com; higher regex_priority wins + local match_t = router:select("GET", "/", "api.example.com") + assert.truthy(match_t) + assert.same(use_case[2].route, match_t.route) + assert.same("*.example.com", match_t.matches.host) + end) + + it_trad_only("selects by created_at when regex_priority ties and multiple wildcard hosts match", function() + local use_case = { + { + service = service, + route = { + id = "e8fb37f1-102d-461e-9c51-6608a6bb8194", + hosts = { "*.com" }, + regex_priority = 100, + created_at = 2000, + }, + }, + { + service = service, + route = { + id = "e8fb37f1-102d-461e-9c51-6608a6bb8195", + hosts = { "*.example.com" }, + regex_priority = 100, + created_at = 1000, + }, + }, + } + + local router = assert(new_router(use_case)) + + -- both match api.example.com, same regex_priority; earlier created_at wins + local match_t = router:select("GET", "/", "api.example.com") + assert.truthy(match_t) + assert.same(use_case[2].route, match_t.route) + end) + end) + describe("[wildcard host]", function() local use_case, router