Skip to content

url matching order changed versus 2.2 #2924

@jfly

Description

@jfly

(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:

Rule('/<path:filename>', endpoint=self.on_static, subdomain="static"),
Rule('/healthcheck', endpoint=self.on_healthcheck, subdomain="<subdomain>"),
cat server.py
from werkzeug.wrappers import Request, Response
from werkzeug.routing import Map, Rule
from werkzeug.exceptions import HTTPException


class SimpleServer:
    def __init__(self, server_name: str, url_map: Map):
        self._server_name = server_name
        self._url_map = url_map

    def dispatch_request(self, request):
        adapter = self._url_map.bind_to_environ(request.environ, server_name=self._server_name)
        try:
            endpoint, values = adapter.match()
            return endpoint(request, **values)
        except HTTPException as e:
            return e

    def wsgi_app(self, environ, start_response):
        request = Request(environ)
        response = self.dispatch_request(request)
        return response(environ, start_response)

    def __call__(self, environ, start_response):
        return self.wsgi_app(environ, start_response)


class Demo(SimpleServer):
    def __init__(self, server_name: str):
        url_map = Map([
            Rule('/<path:filename>', endpoint=self.on_static, subdomain="static"),
            Rule('/healthcheck', endpoint=self.on_healthcheck, subdomain="<subdomain>"),
        ])
        super().__init__(server_name, url_map)

    def on_static(self, _request, filename):
        return Response(f'on_static: {filename=}')

    def on_healthcheck(self, _request, subdomain):
        return Response(f'on_healthcheck {subdomain=}')


if __name__ == '__main__':
    from werkzeug.serving import run_simple

    port = 8080
    app = Demo(server_name=f"example.com:{port}")
    run_simple('127.0.0.1', port, app, use_debugger=True, use_reloader=True)

Before (on werkzeug <2.2)

Run the server:

pip install werkzeug==2.1.2 && python server.py

Note how /healthcheck behaves the same on both the "static" subdomain, and a different subdomain "foo":

"static" subdomain:

$ curl http://static.example.com:8080/healthcheck --resolve '*:8080:127.0.0.1'
on_healthcheck: subdomain='static'

"foo" subdomain:

$ curl http://foo.example.com:8080/healthcheck --resolve '*:8080:127.0.0.1'
on_healthcheck: subdomain='foo'

After (werkzeug 2.2+)

pip install werkzeug==2.2.0 && python server.py

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 the on_healthcheck endpoint.

"static" subdomain:

$ curl http://static.example.com:8080/healthcheck --resolve '*:8080:127.0.0.1'
on_static: filename='healthcheck'

"foo" subdomain:

$ curl http://foo.example.com:8080/healthcheck --resolve '*:8080:127.0.0.1'
on_healthcheck: subdomain='foo'

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:

  • Python version: 3.12.4
  • Werkzeug version: multiple, see description above

@pgjones, since you wrote the new matcher

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions