Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
57 changes: 55 additions & 2 deletions kong/router/traditional.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be and or ~=

Copy link
Copy Markdown
Author

@bakjos bakjos Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if ca and cb then means: it only compare by created_at when both routes have a created_at. If either is nil, you skip the comparison and fall through to return false (no swap). So you only order by time when both values exist.

if ca ~= cb then would mean: When the two created_at values are different, do return ca < cb. So the resulting order could be similar, but the meaning is different: you’re not explicitly requiring both must be set

return ca < cb
end
return false
end)
list[0] = #list
return list
end
return category.routes_by_uris[ctx.hits.uri]
end,

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
41 changes: 41 additions & 0 deletions spec/01-unit/08-router_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading