Skip to content
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

Detect routes that collide during registration #3691

Open
bagerard opened this issue Jan 15, 2022 · 5 comments
Open

Detect routes that collide during registration #3691

bagerard opened this issue Jan 15, 2022 · 5 comments

Comments

@bagerard
Copy link
Contributor

bagerard commented Jan 15, 2022

I've recently been hit by this problem, after changing the ordering in which controllers were registered, some routes weren't called.

This is because some routes using pattern matching were registered before other routes.
For instance route /users/{user_id} registered before /users/export

Of course this can be fixed by changing the ordering in which routes get registered but it is something that is probably bugging other users and that can be detected.

I was able to have a unit test that would detect this in our project (by parsing proutes output) but I was wondering if that is a feature that could be of interest to be baked in pyramid itself? I'd be happy to hack on this, please just point me to a starting point in the codebase

@stevepiercy
Copy link
Member

I think that at least this would be a good recipe for the Community Cookbook if nothing else. The Testing section is pretty thin.

https://docs.pylonsproject.org/projects/pyramid_cookbook/en/latest/testing/index.html

Maybe a new page for Pyramid pytest recipes?

See also https://docs.pylonsproject.org/projects/pyramid/en/latest/designdefense.html#routes-need-relative-ordering and https://docs.pylonsproject.org/projects/pyramid/en/latest/narr/urldispatch.html#route-declaration-ordering

@mmerickel
Copy link
Member

I think it would be hard to craft a patch that we would merge for this. There are so many corner cases where warnings are not desirable when you bring in other features of route configuration that can be used to solve conflicts - namely route predicates.

@bagerard
Copy link
Contributor Author

bagerard commented Jan 16, 2022

Alright no problem, I understand and for sure I don't want to add code that would be difficult to maintain.

What I have is the following (to give a rough idea of how it looks, there are still things to improve in order to make it more generic). It is plug-and-play with the pyramid cookie cutter

test_routes_consistency.py

import re
from dataclasses import dataclass
from typing import List

import pytest
from pyramid.paster import bootstrap
from pyramid.scripts.proutes import get_route_data


@dataclass
class AppRoute:
    name: str
    pattern: str
    view_module: str
    request_methods: str


@pytest.fixture
def pyramid_app_routes(ini_file) -> List[AppRoute]:
    def _get_mapper(registry):
        from pyramid.config import Configurator

        config = Configurator(registry=registry)
        return config.get_routes_mapper()

    env = bootstrap(ini_file, options={})

    registry = env["registry"]
    mapper = _get_mapper(registry)
    raw_routes = mapper.get_routes(include_static=True)

    app_routes = []
    for raw_route in raw_routes:
        route_group = get_route_data(raw_route, registry)
        app_routes += [AppRoute(*route) for route in route_group]

    return app_routes


def test_pyramid_routes_swallowing_due_to_route_registration_ordering_issue(
    pyramid_app_routes,
):
    """The order in which routes are registered matters as explained here
    https://docs.pylonsproject.org/projects/pyramid/en/latest/designdefense.html#routes-need-relative-ordering

    This means that it is important to register specific route BEFORE generic ones

    For instance: it is important to register "/users/export" before "/users/{id}"

    This test aims at detecting generic route that would "swallow" other route, thus
    suggesting a route-ordering problem.
    """

    def route_to_regex_pattern(route):
        regex_matching_route = re.sub(r"{[a-zA-Z]*}", lambda x: "[a-z]+", route)
        return f"^{regex_matching_route}$"

    def route_match_pattern(route, pattern):
        return bool(re.match(pattern, route))

    def is_matching_route(route):
        return all(c in route for c in ("{", "}"))

    clashes = set()
    for idx, route in enumerate(pyramid_app_routes):

        if not is_matching_route(route.pattern):
            continue

        re_pattern = route_to_regex_pattern(route.pattern)

        next_routes = pyramid_app_routes[idx+1:]
        for next_route in next_routes:
            if route.pattern == next_route.pattern:
                continue

            if (
                route_match_pattern(next_route.pattern, pattern=re_pattern)
                and route.request_methods == next_route.request_methods
            ):
                clashes.add(
                    f"The route {route.pattern} ({route.view_module}) is swallowing {next_route.pattern} ({next_route.view_module})"
                )

        assert clashes == set()

Were you suggesting to add a bullet point below "Testing a POST request using cURL" in this page. If that's still what you want, I can open a PR and then we can go through formal code review there of course

@stevepiercy
Copy link
Member

Were you suggesting to add a bullet point below "Testing a POST request using cURL" in this page. If that's still what you want, I can open a PR and then we can go through formal code review there of course

Yes, as well as a link to a new page that contains the recipe.

@huntcsg
Copy link
Contributor

huntcsg commented May 23, 2022

I wonder if this would be an interesting use of an add-on, for the wide number of simple apps that are not using more sophisticated features.

like pyramid_strict_routes that would do some configurable start up validation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants