From 054a911e71a09fcefb41f91fedee89987e04a1a9 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Braun Date: Fri, 22 May 2026 12:54:03 +0200 Subject: [PATCH] fix(api): strip trailing slash on namespace path to avoid '//' rules (#74) When add_namespace(ns, path=path) is called with path='/' (the documented way to mount a namespace at the root) or with any path ending in '/', ns_urls did a naive 'path + url' concatenation, producing rules like '//foo' or '/api//foo'. Normalise the join by stripping the trailing slash on the prefix. Add a regression test that asserts no rule in app.url_map ends up with '//' for path='/' and path='/api/'. Fixes #74. --- CHANGELOG.rst | 1 + flask_restx/api.py | 5 +++++ tests/test_api.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ea0152da..8840168d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -46,6 +46,7 @@ Bug Fixes :: * Adjust field tests for Python 3.14 (``staticmethod`` around ``functools.partial`` used as a class attribute). [python-restx] + * ``Api.add_namespace(ns, path='/')`` (or any path ending in ``/``) no longer produces double-slash rules in the URL map (#74) [jbbqqf] .. _section-1.3.1: 1.3.1 diff --git a/flask_restx/api.py b/flask_restx/api.py index 4f758714..49b419fc 100644 --- a/flask_restx/api.py +++ b/flask_restx/api.py @@ -485,6 +485,11 @@ def get_ns_path(self, ns): def ns_urls(self, ns, urls): path = self.get_ns_path(ns) or ns.path + # Naive `path + url` would produce double slashes when path ends + # in "/" (or is just "/") and url starts with "/". This is the + # documented usage for mounting a namespace at the API root + # (#74), so normalise the join instead of rejecting it. + path = path.rstrip("/") return [path + url for url in urls] def add_namespace(self, ns, path=None): diff --git a/tests/test_api.py b/tests/test_api.py index 2b40b235..b5bed482 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -271,6 +271,36 @@ class TestResource(restx.Resource): with app.test_request_context(): assert url_for("test_resource") == "/api_test/test/" + def test_ns_path_no_double_slash(self, app): + # add_namespace(ns, path=...) used to do a naive `path + url` join, + # so passing path="/" produced rules like "//foo" (see #74), and + # "/api/" similarly produced "/api//foo". Normalise the join so + # neither shape leaks into the URL map. + api = restx.Api() + + ns_root = restx.Namespace("ns_root") + + @ns_root.route("/foo", endpoint="ns_root_foo") + class FooRoot(restx.Resource): + pass + + ns_trailing = restx.Namespace("ns_trailing") + + @ns_trailing.route("/foo", endpoint="ns_trailing_foo") + class FooTrailing(restx.Resource): + pass + + api.add_namespace(ns_root, path="/") + api.add_namespace(ns_trailing, path="/api/") + api.init_app(app) + + rule_paths = sorted(str(r) for r in app.url_map.iter_rules()) + for r in rule_paths: + assert "//" not in r, f"rule has double slash: {r!r}" + with app.test_request_context(): + assert url_for("ns_root_foo") == "/foo" + assert url_for("ns_trailing_foo") == "/api/foo" + def test_multiple_ns_with_authorizations(self, app): api = restx.Api() a1 = {"apikey": {"type": "apiKey", "in": "header", "name": "X-API"}}