Titular issue
Have two rules with a dynamic segment using different converters with the same weight in your application. When a request comes in that could match both rules, it matches the first because it was registered first, as documented:
Rules may end up with the same priority, by having static parts with the same length, and dynamic parts with the same weight, in the same positions. In this case, sorting is stable, so rules added earlier take priority.
Add a new rule in your application before the first (eg for code organization reasons), this rule should have a dynamic segment in the same place as both of the existing rules, and use the same converter as the second rule, but otherwise not match the incoming request's URL.
Incoming requests with the same URL as before now start matching the second rule, despite the fact that it has the same priority as the first rule and that the first rule was added earlier.
Reproduction:
def test_consistent_relative_priority():
rule_1 = r.Rule("/<dummy:value>", endpoint="rule_1")
rule_2 = r.Rule("/<string:value>", endpoint="rule_2")
map = r.Map(
[
rule_1,
rule_2,
], converters={'dummy': r.BaseConverter}
)
adapter = map.bind("example.org", "/")
assert adapter.match("/foo") == ("rule_1", { "value": "foo"})
map = r.Map(
[
r.Rule("/<string:value>/no_match", endpoint="no_match"),
rule_1.empty(),
rule_2.empty(),
], converters={'dummy': r.BaseConverter}
)
adapter = map.bind("example.org", "/")
assert adapter.match("/foo") == ("rule_1", { "value": "foo"}) # Fails: rule_2 matches instead
Note that the dummy converter is just a stand-in for any custom converter with the default weight of 100. If both routes use the string converter in the first position, the issue does not occur because the rules are merged when being added to the map's matcher.
Expected behavior:
Non-matching rules should not affect the relative priority of following rules.
Environment:
- Python version: 3.12.3
- Werkzeug version: 3.0.1 -> main
Related issue
(let me know if I should create a separate issue for this, I'm putting this here because the root cause seems identical to me)
More specific rules will only have priority over more general ones if they use the same converter. When using different converters of the same weight, rules that have the common prefix that was registered first (regardless of the order the actual rules were registered, as demonstrated above) will have priority.
Reproduction:
def test_cross_converter_rule_specificity():
map = r.Map(
[
r.Rule("/<string:value>/<path:path>", endpoint="less_specific"),
r.Rule("/<string:value>/bar", endpoint="more_specific"),
]
)
adapter = map.bind("example.org", "/")
assert adapter.match("/foo/bar") == ("more_specific", { "value": "foo"})
map = r.Map(
[
r.Rule("/<string:value>/<path:path>", endpoint="less_specific"),
r.Rule("/<dummy:value>/bar", endpoint="more_specific"),
], converters={'dummy': r.BaseConverter}
)
adapter = map.bind("example.org", "/")
assert adapter.match("/foo/bar") == ("more_specific", { "value": "foo"}) # Fails: less_specific matches instead
Cause:
I'm not super familiar with the code base but from my understanding, this is caused by the way the StateMachineMatcher merges rules as they're being added, in particular:
# matcher.py L46-49
for test_part, new_state in state.dynamic:
if test_part == part:
state = new_state
break
This code means that when adding a new rule, it takes the priority of whatever existing rule is already registered with which it shares a prefix (for dynamic parts, converters that yield the same RulePart).
Possible fixes
I'm not sure what the proper fix would be, as I'm not super familiar with the codebase and exact implications of each fix, but here are a few suggestions in increasing order of completeness and complexity:
-
Document that converters themselves have a priority, and sort rule parts by weight and by order in which converters were registered on the routing map. This means that custom converters always have lower priority than default converters with the same weight. With this, in the first test, rule_2 will always match regardless of the rule order.
Alternatively, if we want custom converters to win over default ones, it could be in reverse registration order: most recent converter wins, though this goes against the "first wins" principle applied elsewhere.
-
When merging rules in the state machine, only merge the RulePart if it's the same as the last dynamic segment currently known, this fixes the first test case, but not the second, and is more of a band-aid fix, it gives us the priority-by-order property but doesn't fix the specificity problem, and this will result in less merging and likely more expensive matching in pathological cases where users are registering many rules with different converters of the same priority in the same segment in a staggered manner.
-
The most general and complete fix that fixes both tests: merge dynamic parts of the same weight, even if they have different rules, but that means that State.dynamic would now need to be a map of the form {converter_weight: Array[RulePart]}. This has more profound implications w.r.t. how the state machine evaluates matches, as you would in principle need to take multiple state transitions in "parallel" for converters of the same weight, and end up with a sort of DFS/BFS hybrid through the state graph.
Titular issue
Have two rules with a dynamic segment using different converters with the same weight in your application. When a request comes in that could match both rules, it matches the first because it was registered first, as documented:
Add a new rule in your application before the first (eg for code organization reasons), this rule should have a dynamic segment in the same place as both of the existing rules, and use the same converter as the second rule, but otherwise not match the incoming request's URL.
Incoming requests with the same URL as before now start matching the second rule, despite the fact that it has the same priority as the first rule and that the first rule was added earlier.
Reproduction:
Note that the
dummyconverter is just a stand-in for any custom converter with the default weight of 100. If both routes use the string converter in the first position, the issue does not occur because the rules are merged when being added to the map's matcher.Expected behavior:
Non-matching rules should not affect the relative priority of following rules.
Environment:
Related issue
(let me know if I should create a separate issue for this, I'm putting this here because the root cause seems identical to me)
More specific rules will only have priority over more general ones if they use the same converter. When using different converters of the same weight, rules that have the common prefix that was registered first (regardless of the order the actual rules were registered, as demonstrated above) will have priority.
Reproduction:
Cause:
I'm not super familiar with the code base but from my understanding, this is caused by the way the
StateMachineMatchermerges rules as they're being added, in particular:This code means that when adding a new rule, it takes the priority of whatever existing rule is already registered with which it shares a prefix (for dynamic parts, converters that yield the same
RulePart).Possible fixes
I'm not sure what the proper fix would be, as I'm not super familiar with the codebase and exact implications of each fix, but here are a few suggestions in increasing order of completeness and complexity:
Document that converters themselves have a priority, and sort rule parts by weight and by order in which converters were registered on the routing map. This means that custom converters always have lower priority than default converters with the same weight. With this, in the first test, rule_2 will always match regardless of the rule order.
Alternatively, if we want custom converters to win over default ones, it could be in reverse registration order: most recent converter wins, though this goes against the "first wins" principle applied elsewhere.
When merging rules in the state machine, only merge the
RulePartif it's the same as the last dynamic segment currently known, this fixes the first test case, but not the second, and is more of a band-aid fix, it gives us the priority-by-order property but doesn't fix the specificity problem, and this will result in less merging and likely more expensive matching in pathological cases where users are registering many rules with different converters of the same priority in the same segment in a staggered manner.The most general and complete fix that fixes both tests: merge dynamic parts of the same weight, even if they have different rules, but that means that
State.dynamicwould now need to be a map of the form{converter_weight: Array[RulePart]}. This has more profound implications w.r.t. how the state machine evaluates matches, as you would in principle need to take multiple state transitions in "parallel" for converters of the same weight, and end up with a sort of DFS/BFS hybrid through the state graph.