-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
url matching order changed versus 2.2 #2924
Comments
@pgjones can you check this when you get a chance? |
99% This is a bug in the new matcher. werkzeug/src/werkzeug/routing/matcher.py Line 128 in 7868bef
Making the change fixes a similar issue on my end and all tests pass, which indicates that there is a missing test. edit: this is not the issue, false positive related to the structure of the routes I was testing |
Having dug deeper into the issue, I think it comes from a conflict in how RulePart weights are calculated. Currently However, The simplest fix is to switch to use Here is an example of the missing test. def test_static_priority():
map = r.Map(
[
r.Rule("/<path:dyn2>/<dyn1>", endpoint="file"),
r.Rule("/<dyn1>/statn", endpoint="stat"),
],
adapter = map.bind("example.org", "/")
assert adapter.match("/d2/d1", method="GET") == ('file', {'dyn2': 'd2', 'dyn1': 'd1'})
assert adapter.match("/d1/statn", method="GET") == ('stat', {'dyn1': 'd1'}) |
Fix issue pallets#2924 where Rules with dynamic elements would take priority over rules with static elements of the same length. The underlying issue was that the ordering for rules was backwards with regard to number of argument weights. In order to ensure that static elements match first the shortest rule must be tested first. To ensure that a RulePart that contains a path converter does not incorrectly take priority over other RuleParts that are part of other Rules that should have priority, RuleParts containing a path converter are assigned an infinite number of argument weights because they can consume an arbitrary number of url path elements when matching. With this change, two consecutive path converters give priority to the first converter. In general two consecutive path converters with different names cannot have consistent or predicatable behavior (where would you cut?). Tests are updated accordingly. Might consider making back to back path converters an error. closes pallets#2924
Fix issue pallets#2924 where Rules with dynamic elements would take priority over rules with static elements of the same length. The underlying issue was that the ordering for rules was backwards with regard to number of argument weights. In order to ensure that static elements match first the shortest rule must be tested first. To ensure that a RulePart that contains a path converter does not incorrectly take priority over other RuleParts that are part of other Rules that should have priority, RuleParts containing a path converter are assigned an infinite number of argument weights because they can consume an arbitrary number of url path elements when matching. With this change, two consecutive path converters give priority to the first converter. In general two consecutive path converters with different names cannot have consistent or predicatable behavior (where would you cut?). Tests are updated accordingly. Might consider making back to back path converters an error.
I don't think this is deterministic in Werkzeug < 2.2 as if you change your path to (I also don't think #3018 fixes this as a static part will always match in preference to a dynamic part.) |
@pgjones, did you see this question in the OP?
|
Your solution of adding a specific endpoint is correct, but you need to add a default for subdomain since it's not a variable in that rule: Rule(
'/healthcheck',
endpoint=self.on_healthcheck,
subdomain="static",
defaults: {"subdomain": "static"}
) |
@pgjones I can't remember how rules are weighted/sorted. I'm inclined to agree with your analysis, but I think we should at least document this clearly. I'm pretty sure all the following contribute, and are stable, but it's not documented anywhere:
In the following example, I think both rules have two parts, one static and one dynamic each, and the path converter has more weight. But it's not intuitive to know what matching order this results in, or how to know that a rule would be more specific and override a more general rule as shown in the solution above. from werkzeug.routing import Map, Rule
url_map = Map([
Rule("/<path:filename>", endpoint="static", subdomain="static"),
Rule("/healthcheck", endpoint="healthcheck", subdomain="<subdomain>"),
])
adapter = url_map.bind("localhost", subdomain="static", path_info="/healthcheck")
print(adapter.match()) |
(Sorry for the convoluted title: I don't know the right vocabulary for this.)
I noticed when upgrading to Werkzeug 2.2+, two of my routing rules have swapped priority.
Sample application
Here's a simple werkzeug application. The important part is the 2 rules:
cat server.py
Before (on werkzeug <2.2)
Run the server:
Note how /healthcheck behaves the same on both the "static" subdomain, and a different subdomain "foo":
"static" subdomain:
"foo" subdomain:
After (werkzeug 2.2+)
Note how /healthcheck now behaves differently on the two subdomains. On "static", we now get back the
on_static
endpoint, and on "foo" we still get back theon_healthcheck
endpoint."static" subdomain:
"foo" subdomain:
I see the same behavior with the latest version of werkzeug (3.0.3 at time of writing).
Summary
Is this change in behavior intentional? The PR #2433 just describes this as a faster matcher, it doesn't say anything about a change in behavior.
Is there some way of configuring a route across all subdomains that takes precedence over the subdomain specific
/<path:filename>
rule in my example?Workaround
I don't have a great workaround for this. I can get close to the pre-werkzeug 2.2 behavior by adding a 3rd rule specifically for the "static" subdomain:
Rule('/<path:filename>', endpoint=self.on_static, subdomain="static"), Rule('/healthcheck', endpoint=self.on_healthcheck, subdomain="<subdomain>"), +Rule('/healthcheck', endpoint=self.on_healthcheck, subdomain="static"),
But this behaves a bit differently: there's no
subdomain
argument passed to my endpoint handler.Environment:
@pgjones, since you wrote the new matcher
The text was updated successfully, but these errors were encountered: