codecov[bot]: # [Codecov](https://codecov.io/gh/falconry/falcon/pull/1751?src=pr&el=h1) Report
> Merging [#1751](https://codecov.io/gh/falconry/falcon/pull/1751?src=pr&el=desc) into [master](https://codecov.io/gh/falconry/falcon/commit/113eea113844058816ed745d8dc9cb197e75cb95&el=desc) will **not change** coverage.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/falconry/falcon/pull/1751/graphs/tree.svg?width=650&height=150&src=pr&token=zOtkfNQ3h6)](https://codecov.io/gh/falconry/falcon/pull/1751?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##            master     #1751   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           55        55           
  Lines         5037      5037           
  Branches       823       823           
=========================================
  Hits          5037      5037           
```


| [Impacted Files](https://codecov.io/gh/falconry/falcon/pull/1751?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [falcon/routing/compiled.py](https://codecov.io/gh/falconry/falcon/pull/1751/diff?src=pr&el=tree#diff-ZmFsY29uL3JvdXRpbmcvY29tcGlsZWQucHk=) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/falconry/falcon/pull/1751?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/falconry/falcon/pull/1751?src=pr&el=footer). Last update [113eea1...f084fb7](https://codecov.io/gh/falconry/falcon/pull/1751?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

CaselIT: Regarding the question, the problem I see is that we cannot assume that an user has set the options before adding the routes. Also the options may be dynamically updated.
Taking this into consideration, I don't think a warning should be raised, we should just document, maybe with a warning, the behaviour of the router wrt that request option.
vytas7: Ah, good point wrt the configuration order @CaselIT !
[`RequestOptions.strip_url_path_trailing_slash`](https://falcon.readthedocs.io/en/latest/api/app.html#falcon.RequestOptions.strip_url_path_trailing_slash) is by default `False`, so if the user hasn't set that to `True` yet, we obviously cannot do anything about it. But if the user has already overridden to `True`, I am inclined to emit a warning though, that might be helpful in debugging... :thinking:
CaselIT: The documentation looks good 👍
vytas7: @kgriffs good point; I have now added a dedicated test to better exercise this scenario.
diff --git a/docs/_newsfragments/1544.breakingchange.rst b/docs/_newsfragments/1544.breakingchange.rst
new file mode 100644
index 0000000..8728bd5
--- /dev/null
+++ b/docs/_newsfragments/1544.breakingchange.rst
@@ -0,0 +1,6 @@
+If :attr:`RequestOptions.strip_url_path_trailing_slash
+<falcon.RequestOptions.strip_url_path_trailing_slash>` is enabled, routes
+should now be added without a trailing slash. Previously, the trailing slash
+was always removed as a side effect of a bug regardless of the
+:attr:`~falcon.RequestOptions.strip_url_path_trailing_slash` option value.
+See also: :ref:`trailing_slash_in_path`
diff --git a/docs/_newsfragments/1544.bugfix.rst b/docs/_newsfragments/1544.bugfix.rst
new file mode 100644
index 0000000..8810830
--- /dev/null
+++ b/docs/_newsfragments/1544.bugfix.rst
@@ -0,0 +1,6 @@
+Previously, the default :class:`CompiledRouter <falcon.routing.CompiledRouter>`
+was erroneously stripping trailing slashes from URI templates.
+This has been fixed so that it is now possible to add two different routes for
+a path with and without a trailing forward slash (see also:
+:attr:`RequestOptions.strip_url_path_trailing_slash
+<falcon.RequestOptions.strip_url_path_trailing_slash>`).
diff --git a/docs/_newsfragments/1594.bugfix.rst b/docs/_newsfragments/1594.bugfix.rst
index 814c057..2ff0112 100644
--- a/docs/_newsfragments/1594.bugfix.rst
+++ b/docs/_newsfragments/1594.bugfix.rst
@@ -1,3 +1,3 @@
 :meth:`falcon.uri.decode` and :meth:`falcon.uri.parse_query_string` no longer
 explode quadratically for a large number of percent-encoded characters. The
-time complexity of these utility functions is now always close to *O*(*n*).
+time complexity of these utility functions is now always close to *O*\(*n*).
diff --git a/docs/user/faq.rst b/docs/user/faq.rst
index 0988d96..941689e 100644
--- a/docs/user/faq.rst
+++ b/docs/user/faq.rst
@@ -478,6 +478,8 @@ being fully defined by the WSGI spec (PEP-3333). This is discussed in the
 reference documentation for :attr:`~falcon.Request.stream`, and a workaround
 is provided in the form of :attr:`~falcon.Request.bounded_stream`.
 
+.. _trailing_slash_in_path:
+
 How does Falcon handle a trailing slash in the request path?
 ------------------------------------------------------------
 If your app sets :attr:`~falcon.RequestOptions.strip_url_path_trailing_slash` to
@@ -491,6 +493,16 @@ For example, with this option enabled, adding a route for ``'/foo/bar'``
 implicitly adds a route for ``'/foo/bar/'``. In other words, requests coming
 in for either path will be sent to the same resource.
 
+.. warning::
+
+    If :attr:`~falcon.RequestOptions.strip_url_path_trailing_slash` is enabled,
+    adding a route with a trailing slash will effectively make it unreachable
+    from normal routing (theoretically, it may still be matched by rewriting
+    the request path in middleware).
+
+    In this case, routes should be added without a trailing slash (obviously
+    except the root path ``'/'``), such as ``'/foo/bar'`` in the example above.
+
 .. note::
 
     Starting with version 2.0, the default for the
diff --git a/falcon/app.py b/falcon/app.py
index 9c376d5..f3d5a25 100644
--- a/falcon/app.py
+++ b/falcon/app.py
@@ -456,6 +456,13 @@ class App:
 
                 (See also: :meth:`~.App.add_sink`)
 
+                Warning:
+                    If :attr:`~falcon.RequestOptions.strip_url_path_trailing_slash`
+                    is enabled, `uri_template` should be provided without a
+                    trailing slash.
+
+                    (See also: :ref:`trailing_slash_in_path`)
+
             resource (instance): Object which represents a REST
                 resource. Falcon will pass GET requests to ``on_get()``,
                 PUT requests to ``on_put()``, etc. If any HTTP methods are not
diff --git a/falcon/routing/compiled.py b/falcon/routing/compiled.py
index b319884..9bbcb77 100644
--- a/falcon/routing/compiled.py
+++ b/falcon/routing/compiled.py
@@ -198,7 +198,7 @@ class CompiledRouter:
         if re.search(r'\s', _FIELD_PATTERN.sub('{FIELD}', uri_template)):
             raise ValueError('URI templates may not include whitespace.')
 
-        path = uri_template.strip('/').split('/')
+        path = uri_template.lstrip('/').split('/')
 
         used_names = set()
         for segment in path:

Routes with trailing slash do not work under default API Options
Hello,

It seems when using the current default for api.req_options.strip_url_path_trailing_slash which is False, adding a slash at the end of a route doesn't work.

I think I found out what the problem is.

Here is the code I tried:
```python
import falcon

class StaticResource(object):

    def on_get(self, _: falcon.Request, resp: falcon.Response) -> None:
        resp.status = falcon.HTTP_200
        resp.content_type = 'text/plain'
        resp.body = 'static'

def create_app() -> falcon.API:
    api = falcon.API()
    api.req_options.strip_url_path_trailing_slash = False
    api.add_route('/static_route/', StaticResource())
    api.add_route('/static_route2', StaticResource())
    print(api._router._finder_src)
    return api
```

The finder_src compiled function is :
```python
def find(path, return_values, patterns, converters, params):
    path_len = len(path)
    if path_len > 0:
        if path[0] == 'static_route':
            if path_len == 1:
                return return_values[0]
            return None
        if path[0] == 'static_route2':
            if path_len == 1:
                return return_values[1]
            return None
        return None
    return None
```
We see that the code generated for both routes is the same.
The path passed to this function when calling uri: "http://127.0.0.1:8000/static_route/" is : ['static_route', ''] which has a length of 2, leading the function to return None and therefore an HTTPError 404.
When calling uri: "http://127.0.0.1:8000/static_route", the function returns the correct route and leads to HTTP 200.

Therefore using the default option leads to an impossibility to have routes ending with a trailing slash and even worse, when using routes ending with a trailing slash, the route without it works...
diff --git a/tests/test_httperror.py b/tests/test_httperror.py
index 75adc65..55f620f 100644
--- a/tests/test_httperror.py
+++ b/tests/test_httperror.py
@@ -605,7 +605,7 @@ class TestHTTPError:
 
     def test_405_without_body_with_extra_headers_double_check(self, client):
         client.app.add_route(
-            '/405/', MethodNotAllowedResourceWithHeadersWithAccept()
+            '/405', MethodNotAllowedResourceWithHeadersWithAccept()
         )
 
         response = client.simulate_request(path='/405')
diff --git a/tests/test_uri_templates.py b/tests/test_uri_templates.py
index 661edb4..8916f1d 100644
--- a/tests/test_uri_templates.py
+++ b/tests/test_uri_templates.py
@@ -82,6 +82,18 @@ class FileDetailsResource:
         self.called = True
 
 
+class KitchenSinkResource:
+    def __init__(self):
+        self.call_count = 0
+        self.kwargs = None
+        self.uri_template = None
+
+    def on_get(self, req, resp, **kwargs):
+        self.call_count += 1
+        self.kwargs = kwargs
+        self.uri_template = req.uri_template
+
+
 class ResourceWithSuffixRoutes:
     def __init__(self):
         self.get_called = False
@@ -349,21 +361,22 @@ def test_converter_custom(client, resource, uri_template, path, expected):
 def test_single_trailing_slash(client):
     resource1 = IDResource()
     client.app.add_route('/1/{id}/', resource1)
-    result = client.simulate_get('/1/123')
+    assert client.simulate_get('/1/123').status_code == 404
+    result = client.simulate_get('/1/123/')
     assert result.status == falcon.HTTP_200
     assert resource1.called
     assert resource1.id == '123'
-    assert resource1.req.path == '/1/123'
+    assert resource1.req.path == '/1/123/'
 
     resource2 = IDResource()
     client.app.add_route('/2/{id}/', resource2)
-    result = client.simulate_get('/2/123/')
+    result = client.simulate_get('/2/123')
     assert result.status == falcon.HTTP_404
     assert not resource2.called
     assert resource2.id is None
 
     resource3 = IDResource()
-    client.app.add_route('/3/{id}/', resource3)
+    client.app.add_route('/3/{id}', resource3)
     client.app.req_options.strip_url_path_trailing_slash = True
     result = client.simulate_get('/3/123/')
     assert result.status == falcon.HTTP_200
@@ -475,6 +488,34 @@ def test_adding_suffix_routes(client):
     assert resource_with_suffix_routes.collection_id == 'foo321'
 
 
+@pytest.mark.parametrize('reverse', [True, False])
+def test_with_and_without_trailing_slash(client, reverse):
+    routes = [
+        ('/kitchen', KitchenSinkResource()),
+        ('/kitchen/', KitchenSinkResource()),
+        ('/kitchen/{item}', KitchenSinkResource()),
+        ('/kitchen/{item}/', KitchenSinkResource()),
+        ('/kitchen/sink', KitchenSinkResource()),
+        ('/kitchen/sink/', KitchenSinkResource()),
+    ]
+    if reverse:
+        routes.reverse()
+
+    for route in routes:
+        client.app.add_route(*route)
+
+    for uri_template, resource in routes:
+        item = None
+        if '{item}' in uri_template:
+            item = 'kettle' if uri_template.endswith('/') else 'teapot'
+        resp = client.simulate_get(uri_template.replace('{item}', item or ''))
+
+        assert resp.status_code == 200
+        assert resource.call_count == 1
+        assert resource.kwargs.get('item') == item
+        assert resource.uri_template == uri_template
+
+
 def test_custom_error_on_suffix_route_not_found(client):
     resource_with_suffix_routes = ResourceWithSuffixRoutes()
     with pytest.raises(SuffixedMethodNotFoundError):

pip install -e .
parse_log_pytest
requirements.txt
apt-get update apt-get install -y gcc
3.9
requirements/tests
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
aiofiles==24.1.0
anyio==4.9.0
asgiref==3.8.1
attrs==25.3.0
autobahn==24.4.2
Automat==24.8.1
certifi==2025.1.31
cffi==1.17.1
charset-normalizer==3.4.1
click==8.1.8
constantly==23.10.4
coverage==7.8.0
cryptography==44.0.2
daphne==4.1.2
exceptiongroup==1.2.2
-e git+https://github.com/falconry/falcon.git@ff4d231855e08ebf4456ae74f08018686db6860b#egg=falcon
h11==0.14.0
httpcore==1.0.7
httpx==0.28.1
hyperlink==21.0.0
idna==3.10
incremental==24.7.2
iniconfig==2.1.0
msgpack==1.1.0
mujson==1.4
orjson==3.10.16
packaging==24.2
pluggy==1.5.0
pyasn1==0.6.1
pyasn1_modules==0.4.2
pycparser==2.22
pyOpenSSL==25.0.0
pytest==8.3.5
python-rapidjson==1.20
PyYAML==6.0.2
requests==2.32.3
service-identity==24.2.0
sniffio==1.3.1
testtools==2.7.2
tomli==2.2.1
Twisted==24.11.0
txaio==23.1.1
typing_extensions==4.13.0
ujson==5.10.0
urllib3==2.3.0
uvicorn==0.34.0
zope.interface==7.2

name: falcon
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - aiofiles==24.1.0
      - anyio==4.9.0
      - asgiref==3.8.1
      - attrs==25.3.0
      - autobahn==24.4.2
      - automat==24.8.1
      - certifi==2025.1.31
      - cffi==1.17.1
      - charset-normalizer==3.4.1
      - click==8.1.8
      - constantly==23.10.4
      - coverage==7.8.0
      - cryptography==44.0.2
      - daphne==4.1.2
      - exceptiongroup==1.2.2
      - falcon==3.0.0a2
      - h11==0.14.0
      - httpcore==1.0.7
      - httpx==0.28.1
      - hyperlink==21.0.0
      - idna==3.10
      - incremental==24.7.2
      - iniconfig==2.1.0
      - msgpack==1.1.0
      - mujson==1.4
      - orjson==3.10.16
      - packaging==24.2
      - pluggy==1.5.0
      - pyasn1==0.6.1
      - pyasn1-modules==0.4.2
      - pycparser==2.22
      - pyopenssl==25.0.0
      - pytest==8.3.5
      - python-rapidjson==1.20
      - pyyaml==6.0.2
      - requests==2.32.3
      - service-identity==24.2.0
      - sniffio==1.3.1
      - testtools==2.7.2
      - tomli==2.2.1
      - twisted==24.11.0
      - txaio==23.1.1
      - typing-extensions==4.13.0
      - ujson==5.10.0
      - urllib3==2.3.0
      - uvicorn==0.34.0
      - zope-interface==7.2
prefix: /opt/conda/envs/falcon

tests/test_uri_templates.py::test_uuid_converter[wsgi-/widgets/{widget_id:uuid}-/widgets/43fb3151-691d-432f-aa6a-520bd6746924-expected0] tests/test_uri_templates.py::test_uuid_converter[wsgi-/widgets/{widget_id:uuid}/orders-/widgets/43fb3151691d432faa6a520bd6746924/orders-expected1] tests/test_uri_templates.py::test_uuid_converter[wsgi-/versions/diff/{left:uuid()}...{right:uuid()}-/versions/diff/43fb3151-691d-432f-aa6a-520bd6746924...1d631cb6-d4eb-4c5e-9543-2479300f9f6c-expected2] tests/test_uri_templates.py::test_uuid_converter[wsgi-/versions/diff/{left:uuid}...{right:uuid()}-/versions/diff/43fb3151-691d-432f-aa6a-520bd6746924...1d631cb6-d4eb-4c5e-9543-2479300f9f6c-expected3] tests/test_uri_templates.py::test_uuid_converter[wsgi-/versions/diff/{left:uuid()}...{right:uuid}-/versions/diff/43fb3151-691d-432f-aa6a-520bd6746924...1d631cb6-d4eb-4c5e-9543-2479300f9f6c-expected4] tests/test_uri_templates.py::test_uuid_converter[wsgi-/widgets/{widget_id:uuid}/orders-/widgets/43fb3151691d432faa6a520bd674692/orders-None] tests/test_uri_templates.py::test_single_trailing_slash[wsgi] tests/test_uri_templates.py::test_with_and_without_trailing_slash[wsgi-True] tests/test_uri_templates.py::test_with_and_without_trailing_slash[wsgi-False]
tests/test_uri_templates.py::test_root_path[asgi] tests/test_uri_templates.py::test_no_vars[asgi] tests/test_uri_templates.py::test_special_chars[asgi] tests/test_uri_templates.py::test_single[asgi-id] tests/test_uri_templates.py::test_single[asgi-id123] tests/test_uri_templates.py::test_single[asgi-widget_id] tests/test_uri_templates.py::test_single_path_segment[asgi] tests/test_uri_templates.py::test_int_converter[asgi-/{id:int}] tests/test_uri_templates.py::test_int_converter[asgi-/{id:int(3)}] tests/test_uri_templates.py::test_int_converter[asgi-/{id:int(min=123)}] tests/test_uri_templates.py::test_int_converter[asgi-/{id:int(min=123, tests/test_uri_templates.py::test_int_converter_rejections[asgi-/{id:int(2)}] tests/test_uri_templates.py::test_int_converter_rejections[asgi-/{id:int(min=124)}] tests/test_uri_templates.py::test_int_converter_rejections[asgi-/{id:int(num_digits=3, tests/test_uri_templates.py::test_datetime_converter[asgi-/{start_year:int}-to-{timestamp:dt}-/1961-to-1969-07-21T02:56:00Z-dt_expected0] tests/test_uri_templates.py::test_datetime_converter[asgi-/{start_year:int}-to-{timestamp:dt("%Y-%m-%d")}-/1961-to-1969-07-21-dt_expected1] tests/test_uri_templates.py::test_datetime_converter[asgi-/{start_year:int}/{timestamp:dt("%Y-%m-%d tests/test_uri_templates.py::test_datetime_converter[asgi-/{start_year:int}-to-{timestamp:dt("%Y-%m")}-/1961-to-1969-07-21-None] tests/test_uri_templates.py::test_uuid_converter[asgi-/widgets/{widget_id:uuid}-/widgets/43fb3151-691d-432f-aa6a-520bd6746924-expected0] tests/test_uri_templates.py::test_uuid_converter[asgi-/widgets/{widget_id:uuid}/orders-/widgets/43fb3151691d432faa6a520bd6746924/orders-expected1] tests/test_uri_templates.py::test_uuid_converter[asgi-/versions/diff/{left:uuid()}...{right:uuid()}-/versions/diff/43fb3151-691d-432f-aa6a-520bd6746924...1d631cb6-d4eb-4c5e-9543-2479300f9f6c-expected2] tests/test_uri_templates.py::test_uuid_converter[asgi-/versions/diff/{left:uuid}...{right:uuid()}-/versions/diff/43fb3151-691d-432f-aa6a-520bd6746924...1d631cb6-d4eb-4c5e-9543-2479300f9f6c-expected3] tests/test_uri_templates.py::test_uuid_converter[asgi-/versions/diff/{left:uuid()}...{right:uuid}-/versions/diff/43fb3151-691d-432f-aa6a-520bd6746924...1d631cb6-d4eb-4c5e-9543-2479300f9f6c-expected4] tests/test_uri_templates.py::test_uuid_converter[asgi-/widgets/{widget_id:uuid}/orders-/widgets/43fb3151691d432faa6a520bd674692/orders-None] tests/test_uri_templates.py::test_uuid_converter_complex_segment[asgi] tests/test_uri_templates.py::test_converter_custom[asgi-/{food:spam}-/something-expected0] tests/test_uri_templates.py::test_converter_custom[asgi-/{food:spam(")")}:{food_too:spam("()")}-/bacon:eggs-expected1] tests/test_uri_templates.py::test_converter_custom[asgi-/({food:spam()}){food_too:spam("()")}-/(bacon)eggs-expected2] tests/test_uri_templates.py::test_single_trailing_slash[asgi] tests/test_uri_templates.py::test_multiple[asgi] tests/test_uri_templates.py::test_same_level_complex_var[asgi-True] tests/test_uri_templates.py::test_same_level_complex_var[asgi-False] tests/test_uri_templates.py::test_adding_suffix_routes[asgi] tests/test_uri_templates.py::test_with_and_without_trailing_slash[asgi-True] tests/test_uri_templates.py::test_with_and_without_trailing_slash[asgi-False]
tests/test_httperror.py::TestHTTPError::test_base_class[wsgi] tests/test_httperror.py::TestHTTPError::test_has_representation tests/test_httperror.py::TestHTTPError::test_no_description_json[wsgi] tests/test_httperror.py::TestHTTPError::test_no_description_xml[wsgi] tests/test_httperror.py::TestHTTPError::test_client_does_not_accept_json_or_xml[wsgi] tests/test_httperror.py::TestHTTPError::test_custom_error_serializer[wsgi] tests/test_httperror.py::TestHTTPError::test_custom_error_serializer_optional_representation[wsgi-GET-/404-404] tests/test_httperror.py::TestHTTPError::test_custom_error_serializer_optional_representation[wsgi-GET-/notfound-404] tests/test_httperror.py::TestHTTPError::test_custom_error_serializer_optional_representation[wsgi-REPORT-/404-405] tests/test_httperror.py::TestHTTPError::test_custom_error_serializer_optional_representation[wsgi-BREW-/notfound-400] tests/test_httperror.py::TestHTTPError::test_custom_serializer_no_representation[wsgi] tests/test_httperror.py::TestHTTPError::test_client_does_not_accept_anything[wsgi] tests/test_httperror.py::TestHTTPError::test_forbidden[wsgi-application/json] tests/test_httperror.py::TestHTTPError::test_forbidden[wsgi-application/vnd.company.system.project.resource+json;v=1.1] tests/test_httperror.py::TestHTTPError::test_forbidden[wsgi-application/json-patch+json] tests/test_httperror.py::TestHTTPError::test_epic_fail_json[wsgi] tests/test_httperror.py::TestHTTPError::test_epic_fail_xml[wsgi-text/xml] tests/test_httperror.py::TestHTTPError::test_epic_fail_xml[wsgi-application/xml] tests/test_httperror.py::TestHTTPError::test_epic_fail_xml[wsgi-application/vnd.company.system.project.resource+xml;v=1.1] tests/test_httperror.py::TestHTTPError::test_epic_fail_xml[wsgi-application/atom+xml] tests/test_httperror.py::TestHTTPError::test_unicode_json[wsgi] tests/test_httperror.py::TestHTTPError::test_unicode_xml[wsgi] tests/test_httperror.py::TestHTTPError::test_401[wsgi] tests/test_httperror.py::TestHTTPError::test_404_without_body[wsgi] tests/test_httperror.py::TestHTTPError::test_404_with_body[wsgi] tests/test_httperror.py::TestHTTPError::test_405_without_body[wsgi] tests/test_httperror.py::TestHTTPError::test_405_without_body_with_extra_headers[wsgi] tests/test_httperror.py::TestHTTPError::test_405_without_body_with_extra_headers_double_check[wsgi] tests/test_httperror.py::TestHTTPError::test_405_with_body[wsgi] tests/test_httperror.py::TestHTTPError::test_410_without_body[wsgi] tests/test_httperror.py::TestHTTPError::test_410_with_body[wsgi] tests/test_httperror.py::TestHTTPError::test_411[wsgi] tests/test_httperror.py::TestHTTPError::test_413[wsgi] tests/test_httperror.py::TestHTTPError::test_temporary_413_integer_retry_after[wsgi] tests/test_httperror.py::TestHTTPError::test_temporary_413_datetime_retry_after[wsgi] tests/test_httperror.py::TestHTTPError::test_414[wsgi] tests/test_httperror.py::TestHTTPError::test_414_with_title[wsgi] tests/test_httperror.py::TestHTTPError::test_414_with_description[wsgi] tests/test_httperror.py::TestHTTPError::test_414_with_custom_kwargs[wsgi] tests/test_httperror.py::TestHTTPError::test_416[wsgi] tests/test_httperror.py::TestHTTPError::test_429_no_retry_after[wsgi] tests/test_httperror.py::TestHTTPError::test_429[wsgi] tests/test_httperror.py::TestHTTPError::test_429_datetime[wsgi] tests/test_httperror.py::TestHTTPError::test_503_integer_retry_after[wsgi] tests/test_httperror.py::TestHTTPError::test_503_datetime_retry_after[wsgi] tests/test_httperror.py::TestHTTPError::test_invalid_header[wsgi] tests/test_httperror.py::TestHTTPError::test_missing_header[wsgi] tests/test_httperror.py::TestHTTPError::test_invalid_param[wsgi] tests/test_httperror.py::TestHTTPError::test_missing_param[wsgi] tests/test_httperror.py::TestHTTPError::test_misc[wsgi] tests/test_httperror.py::TestHTTPError::test_title_default_message_if_none[wsgi] tests/test_httperror.py::test_kw_only tests/test_httperror.py::test_NoRepresentation tests/test_httperror.py::TestOptionalRepresentation::test_OptionalRepresentation_false tests/test_httperror.py::TestOptionalRepresentation::test_OptionalRepresentation_true tests/test_uri_templates.py::test_root_path[wsgi] tests/test_uri_templates.py::test_no_vars[wsgi] tests/test_uri_templates.py::test_special_chars[wsgi] tests/test_uri_templates.py::test_single[wsgi-id] tests/test_uri_templates.py::test_single[wsgi-id123] tests/test_uri_templates.py::test_single[wsgi-widget_id] tests/test_uri_templates.py::test_single_path_segment[wsgi] tests/test_uri_templates.py::test_int_converter[wsgi-/{id:int}] tests/test_uri_templates.py::test_int_converter[wsgi-/{id:int(3)}] tests/test_uri_templates.py::test_int_converter[wsgi-/{id:int(min=123)}] tests/test_uri_templates.py::test_int_converter[wsgi-/{id:int(min=123, tests/test_uri_templates.py::test_int_converter_rejections[wsgi-/{id:int(2)}] tests/test_uri_templates.py::test_int_converter_rejections[wsgi-/{id:int(min=124)}] tests/test_uri_templates.py::test_int_converter_rejections[wsgi-/{id:int(num_digits=3, tests/test_uri_templates.py::test_datetime_converter[wsgi-/{start_year:int}-to-{timestamp:dt}-/1961-to-1969-07-21T02:56:00Z-dt_expected0] tests/test_uri_templates.py::test_datetime_converter[wsgi-/{start_year:int}-to-{timestamp:dt("%Y-%m-%d")}-/1961-to-1969-07-21-dt_expected1] tests/test_uri_templates.py::test_datetime_converter[wsgi-/{start_year:int}/{timestamp:dt("%Y-%m-%d tests/test_uri_templates.py::test_datetime_converter[wsgi-/{start_year:int}-to-{timestamp:dt("%Y-%m")}-/1961-to-1969-07-21-None] tests/test_uri_templates.py::test_uuid_converter_complex_segment[wsgi] tests/test_uri_templates.py::test_converter_custom[wsgi-/{food:spam}-/something-expected0] tests/test_uri_templates.py::test_converter_custom[wsgi-/{food:spam(")")}:{food_too:spam("()")}-/bacon:eggs-expected1] tests/test_uri_templates.py::test_converter_custom[wsgi-/({food:spam()}){food_too:spam("()")}-/(bacon)eggs-expected2] tests/test_uri_templates.py::test_multiple[wsgi] tests/test_uri_templates.py::test_empty_path_component[asgi-//] tests/test_uri_templates.py::test_empty_path_component[asgi-//begin] tests/test_uri_templates.py::test_empty_path_component[asgi-/end//] tests/test_uri_templates.py::test_empty_path_component[asgi-/in//side] tests/test_uri_templates.py::test_empty_path_component[wsgi-//] tests/test_uri_templates.py::test_empty_path_component[wsgi-//begin] tests/test_uri_templates.py::test_empty_path_component[wsgi-/end//] tests/test_uri_templates.py::test_empty_path_component[wsgi-/in//side] tests/test_uri_templates.py::test_relative_path[asgi-] tests/test_uri_templates.py::test_relative_path[asgi-no] tests/test_uri_templates.py::test_relative_path[asgi-no/leading_slash] tests/test_uri_templates.py::test_relative_path[wsgi-] tests/test_uri_templates.py::test_relative_path[wsgi-no] tests/test_uri_templates.py::test_relative_path[wsgi-no/leading_slash] tests/test_uri_templates.py::test_same_level_complex_var[wsgi-True] tests/test_uri_templates.py::test_same_level_complex_var[wsgi-False] tests/test_uri_templates.py::test_adding_suffix_routes[wsgi] tests/test_uri_templates.py::test_custom_error_on_suffix_route_not_found[asgi] tests/test_uri_templates.py::test_custom_error_on_suffix_route_not_found[wsgi]
docs/user/faq.rst docs/_newsfragments/1544.breakingchange.rst falcon/app.py docs/_newsfragments/1594.bugfix.rst falcon/routing/compiled.py docs/_newsfragments/1544.bugfix.rst
docs/user/faq.rst docs/_newsfragments/1544.breakingchange.rst falcon/app.py docs/_newsfragments/1594.bugfix.rst falcon/routing/compiled.py docs/_newsfragments/1544.bugfix.rst