sanic-org / sanic-routing

Internal handler routing for Sanic beginning with v21.3.

Home Page:https://sanicframework.org/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Parameterized Route returns 404 instead of 405

eric-spitler opened this issue · comments

Observation / Problem

When a route is parametrized (e.g. /<identifier> or /<identifier:int>) and an HTTP method is used that is not defined, the response comes back as a 404 Not Found instead of 405 Method Not Allowed.

Simple routes w/o parameterization properly return a 405.

Test Setup

from sanic import Sanic, Blueprint
from sanic.response import HTTPResponse
from sanic.views import HTTPMethodView

app = Sanic(name='test')


@app.get(uri='/with-func', name='get-func')
def handler_str(request):
    print(None)
    return HTTPResponse()


@app.get(uri='/with-func/str/<identifier>', name='str-id-get-func')
def handler_str(request, identifier: str):
    print(identifier, type(identifier))
    return HTTPResponse()


@app.get(uri='/with-func/int/<identifier:int>', name='int-id-get-func')
def handler_int(request, identifier: int):
    print(identifier, type(identifier))
    return HTTPResponse()


class StrView(HTTPMethodView, attach=app, uri='/with-view', name='get-view'):
    @staticmethod
    def get(request):
        print(None)
        return HTTPResponse()


class StrView(HTTPMethodView, attach=app, uri='/with-view/str/<identifier>', name='str-id-get-view'):
    @staticmethod
    def get(request, identifier):
        print(identifier, type(identifier))
        return HTTPResponse()


class IntView(HTTPMethodView, attach=app, uri='/with-view/int/<identifier:int>', name='int-id-get-view'):
    @staticmethod
    def get(request, identifier: int):
        print(identifier, type(identifier))
        return HTTPResponse()


app.run(host='localhost', port=8080, single_process=True)

Test Execution

from httpx import get, post

paths = [
    '/with-func',
    '/with-func/str/test',
    '/with-func/int/1',
    '/with-view',
    '/with-view/str/test',
    '/with-view/int/1'
]

for path in paths:
    print(path)

    for method in [get, post]:
        response = method(f'http://localhost:8080{path}')
        print(f'\t{method.__name__:8}{response}')

App Logs

[2023-11-02 21:35:08 +0000] [32026] [INFO] Sanic v23.6.0
[2023-11-02 21:35:08 +0000] [32026] [INFO] Goin' Fast @ http://localhost:8080
[2023-11-02 21:35:08 +0000] [32026] [INFO] mode: production, single worker
[2023-11-02 21:35:08 +0000] [32026] [INFO] server: sanic, HTTP/1.1
[2023-11-02 21:35:08 +0000] [32026] [INFO] python: 3.11.6
[2023-11-02 21:35:08 +0000] [32026] [INFO] platform: Linux-3.10.0-1160.99.1.el7.x86_64-x86_64-with-glibc2.17
[2023-11-02 21:35:08 +0000] [32026] [INFO] packages: sanic-routing==23.6.0, sanic-testing==23.6.0, sanic-ext==23.6.0
[2023-11-02 21:35:08 +0000] [32026] [INFO] Sanic Extensions:
[2023-11-02 21:35:08 +0000] [32026] [INFO]   > injection [0 dependencies; 0 constants]
[2023-11-02 21:35:08 +0000] [32026] [INFO]   > openapi [http://localhost:8080/docs]
[2023-11-02 21:35:08 +0000] [32026] [INFO]   > http 
[2023-11-02 21:35:08 +0000] [32026] [INFO] Starting worker [32026]
None
test <class 'str'>
1 <class 'int'>
None
test <class 'str'>
1 <class 'int'>

Test Logs

/with-func
	get     <Response [200 OK]>
	post    <Response [405 Method Not Allowed]>
/with-func/str/test
	get     <Response [200 OK]>
	post    <Response [404 Not Found]>
/with-func/int/1
	get     <Response [200 OK]>
	post    <Response [404 Not Found]>
/with-view
	get     <Response [200 OK]>
	post    <Response [405 Method Not Allowed]>
/with-view/str/test
	get     <Response [200 OK]>
	post    <Response [404 Not Found]>
/with-view/int/1
	get     <Response [200 OK]>
	post    <Response [404 Not Found]>

Cause

This section catches NotFound and NoMethod

except (NotFound, NoMethod) as e:
# If we did not find the route, we might need to try routing one
# more time to handle strict_slashes
if path.endswith(self.delimiter):
return self.resolve(
path=path[:-1],
method=method,
orig=path,
extra=extra,
)
raise self.exception(str(e), path=path)

but the value of self.exception is <class 'sanic_routing.exceptions.NotFound'>

Suggestion

Add a path parameter to the NoMethod class

class NoMethod(BaseException):
def __init__(
self,
message: str = "Method does not exist",
method: Optional[str] = None,
allowed_methods: Optional[Set[str]] = None,
):
super().__init__(message)
self.method = method
self.allowed_methods = allowed_methods

and use the caught exception class to re-raise

            raise e.__class__(str(e), path=path)

Thanks for bringing this up. I am stunned this has not been caught and there is no automated test for this already.