falconry / falcon

The no-magic web data plane API and microservices framework for Python developers, with a focus on reliability, correctness, and performance at scale.
https://falcon.readthedocs.io/en/stable/
Apache License 2.0
9.51k stars 937 forks source link

Method decorator in ASGI causes "Method Not Allowed" error #2009

Closed zoltan-fedor closed 2 years ago

zoltan-fedor commented 2 years ago

Defining a decorator on top of an async endpoint will result in "Method Not Allowed" error.

You will get 405 Method Not Allowed when running:

import falcon.asgi
import logging

logging.basicConfig()
logging.getLogger().setLevel(logging.DEBUG)

def test():
    async def wrap1(func, *args):
        async def limit_wrap(cls, req, resp, *args, **kwargs):
            await func(cls, req, resp, *args, **kwargs)
        return limit_wrap
    return wrap1

class ThingsResource:
    @test()
    async def on_get(self, req, resp):
        resp.body = 'Hello world!'

app = falcon.asgi.App()
things = ThingsResource()
app.add_route('/things', things)

That error pops, because the responder is not a callable, see https://github.com/falconry/falcon/blob/818698b7ca63f239642cf6d705109d720e8c6782/falcon/routing/util.py#L137

Even if that line (falcon/routing/util.py#L137) gets taken out, I still get the next error:

ERROR:falcon:[FALCON] Unhandled exception in ASGI app
Traceback (most recent call last):
  File "/home/user/.local/share/virtualenvs/falcon-limiter-test-app-async-eh6amfJm/lib/python3.8/site-packages/falcon/asgi/app.py", line 408, in __call__
    await responder(req, resp, **params)
TypeError: 'coroutine' object is not callable

The same thing works with WSGI (no async):

import falcon

def test():
    def wrap1(func, *args):
        def limit_wrap(cls, req, resp, *args, **kwargs):
            func(cls, req, resp, *args, **kwargs)
        return limit_wrap
    return wrap1

class ThingsResource:
    @test()
    def on_get(self, req, resp):
        resp.body = 'Hello world!'

app = falcon.API()
things = ThingsResource()
app.add_route('/things', things)

How can decorators be added to the async view functions? Thanks!

Note: The reason for asking about this, because I trying to release a new version for the Falcon-limiter package to support Falcon ASGI.

vytas7 commented 2 years ago

Hi @zoltan-fedor! Hard to tell without knowing more context, but could the reason be as mundane as an extraneous async on the decorator itself? Python decorators that transform (i.e., decorate) the provided function are executed synchronously at the compile time.

The following works for me:

import falcon
import falcon.asgi
import logging

logging.basicConfig()
logging.getLogger().setLevel(logging.DEBUG)

def test():
    def wrap1(func, *args):
        async def limit_wrap(cls, req, resp, *args, **kwargs):
            await func(cls, req, resp, *args, **kwargs)
        return limit_wrap
    return wrap1

class ThingsResource:
    @test()
    async def on_get(self, req, resp):
        resp.content_type = falcon.MEDIA_TEXT
        resp.text = 'Hello world!\n'

app = falcon.asgi.App()
things = ThingsResource()
app.add_route('/things', things)

When running with uvicorn test:app:

http http://localhost:8000/things
HTTP/1.1 200 OK
content-length: 13
content-type: text/plain; charset=utf-8
date: Sat, 22 Jan 2022 23:00:00 GMT
server: uvicorn

Hello world!

(Note the changed definition of def wrap1(...).)

zoltan-fedor commented 2 years ago

Hi @vytas7, You are absolutely right, it does work without that extra async! It seems it was just a simple oversight on my side. Apologies and thanks for the quick response!

vytas7 commented 2 years ago

No worries @zoltan-fedor, that was a great MRE snippet without any dependencies, so it was dead easy to simply pipe it to a file and run myself. This happens to everyone, at times you just need someone else to look at the code or at least rubberduck it.