Sentry-python: SentryAsgiMiddleware not compatible with Uvicorn 0.13.0

Created on 9 Dec 2020  Â·  28Comments  Â·  Source: getsentry/sentry-python

On December 8th Uvicorn updated from 0.12.3 to 0.13.0. This results in an error at startup, see output with minimal example. When downgrading Uvicorn to 0.12.3 the example runs fine.

Why this error is thrown or which changes resulted in the error, I have no clue. Could you help me with this?

app.py

from sanic import Sanic
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware

app = SentryAsgiMiddleware(Sanic(__name__))

requirements.txt

sanic==20.9.1
sentry-sdk==0.19.4
uvicorn==0.13.0

Command to run:

uvicorn app:app --port 5000 --workers=1 --debug --reload

Output:

INFO:     Uvicorn running on http://127.0.0.1:5000 (Press CTRL+C to quit)
INFO:     Started reloader process [614068] using statreload
Process SpawnProcess-1:
Traceback (most recent call last):
  File "/usr/lib/python3.9/multiprocessing/process.py", line 315, in _bootstrap
    self.run()
  File "/usr/lib/python3.9/multiprocessing/process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
  File "/home/lander/.local/share/virtualenvs/san-iJ5wdX60/lib/python3.9/site-packages/uvicorn/subprocess.py", line 61, in subprocess_started
    target(sockets=sockets)
  File "/home/lander/.local/share/virtualenvs/san-iJ5wdX60/lib/python3.9/site-packages/uvicorn/server.py", line 48, in run
    loop.run_until_complete(self.serve(sockets=sockets))
  File "uvloop/loop.pyx", line 1456, in uvloop.loop.Loop.run_until_complete
  File "/home/lander/.local/share/virtualenvs/san-iJ5wdX60/lib/python3.9/site-packages/uvicorn/server.py", line 55, in serve
    config.load()
  File "/home/lander/.local/share/virtualenvs/san-iJ5wdX60/lib/python3.9/site-packages/uvicorn/config.py", line 319, in load
    elif not inspect.signature(self.loaded_app).parameters:
  File "/usr/lib/python3.9/inspect.py", line 3118, in signature
    return Signature.from_callable(obj, follow_wrapped=follow_wrapped)
  File "/usr/lib/python3.9/inspect.py", line 2867, in from_callable
    return _signature_from_callable(obj, sigcls=cls,
  File "/usr/lib/python3.9/inspect.py", line 2409, in _signature_from_callable
    sig = _signature_from_callable(
  File "/usr/lib/python3.9/inspect.py", line 2242, in _signature_from_callable
    raise TypeError('{!r} is not a callable object'.format(obj))
TypeError: <member '__call__' of 'SentryAsgiMiddleware' objects> is not a callable object
bug

Most helpful comment

I don't see how the middleware exposes itself as app factory.

It doesn't, but due to the __call__ trick the code that Uvicorn was using to decide whether an app was a factory or not was failing. :-)

All 28 comments

Does Sanic expose asgi-2 interface or asgi-3? Our detection may be off by a bit. I would append ._run_asgi2 or ._run_asgi3 to that last line and see which one works:

app = SentryAsgiMiddleware(Sanic(__name__))._run_asgi2
app = SentryAsgiMiddleware(Sanic(__name__))._run_asgi3

@untitaker thanks for the reply!

Both of them seem to successfully start up, a log is presented that the ASGI 'lifespan' protocol is not supported:

INFO:     ASGI 'lifespan' protocol appears unsupported.

Once I try to create request:

from sanic import Sanic
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware


def create_app():
    app = Sanic()

    @app.get("/")
    def root(request):
        return "OK"

    return app


app = SentryAsgiMiddleware(create_app())._run_asgi3

I get the following error / logs:

INFO:     Uvicorn running on http://127.0.0.1:5000 (Press CTRL+C to quit)
INFO:     Started reloader process [49384] using statreload
INFO:     Started server process [49386]
INFO:     Waiting for application startup.
INFO:     ASGI 'lifespan' protocol appears unsupported.
INFO:     Application startup complete.
INFO:     127.0.0.1:35010 - "GET / HTTP/1.1" 500 Internal Server Error
ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "/home/lander/.local/share/virtualenvs/sanic-uvicorn-XMPCd-qp/lib/python3.9/site-packages/uvicorn/protocols/http/httptools_impl.py", line 396, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "/home/lander/.local/share/virtualenvs/sanic-uvicorn-XMPCd-qp/lib/python3.9/site-packages/uvicorn/middleware/proxy_headers.py", line 45, in __call__
    return await self.app(scope, receive, send)
  File "/home/lander/.local/share/virtualenvs/sanic-uvicorn-XMPCd-qp/lib/python3.9/site-packages/uvicorn/middleware/debug.py", line 96, in __call__
    raise exc from None
  File "/home/lander/.local/share/virtualenvs/sanic-uvicorn-XMPCd-qp/lib/python3.9/site-packages/uvicorn/middleware/debug.py", line 78, in __call__
    await self.app(scope, receive, inner_send)
  File "/home/lander/.local/share/virtualenvs/sanic-uvicorn-XMPCd-qp/lib/python3.9/site-packages/uvicorn/middleware/asgi2.py", line 6, in __call__
    instance = self.app(scope)
TypeError: _run_asgi3() missing 2 required positional arguments: 'receive' and 'send'

Right, then you need _run_asgi2.

Both the _run_asgi2 and the _run_asgi3 raise the same exception when requesting the webserver. Is there something I need to add?

I tried editing the __call__ variable of SentryAsgiMiddleware in the source code to setup the callback as _run_asgi3 or as _run_asgi2. Once I run the application with Uvicorn, it crashes at startup, with the initial issue of a not callable object.

Was able to get it running with .run_asgi3 and --interface asgi3 passed as CLI flag.

I will keep this issue open but I think we may just drop ASGI 2 support instead of actually fixing this issue. There's too much inspection magic going on inside of the SDK and inside of uvicorn to figure out with which arguments to call the app.

Great, works indeed. Thanks!

I am getting the same problem, but this time with gunicorn api:app -w 1 -k uvicorn.workers.UvicornWorker
This is the recommended production environment for Starlette.
However, the gunicorn doesn't allow to select asgi2/asgi3 as uvicorn does, and so I get the "missing positional arguments" error, if I use either _run_asgi2or _run_asgi3 on my Middleware.

Interestingly, I don't seem to need to actually call the middleware.
Just changing my file to this:

sentry_sdk.init(dsn=settings.SENTRY_DSN, traces_sample_rate=1.0)
dummy_app = SentryAsgiMiddleware(app)

seems to catch errors and report them, even if no-one is actually calling dummy_app. It is as though Sentry somehow hooks it self into the app....

@kristjanvalur then the middleware is not the one doing the error-capturing. But that's fine, its primary purpose is to attach request data to events. Error handling may be done via error logs as well.

I'm using FastAPI.
You are right, without instancing the middleware, I still get the errors delivered. It seems sufficient to init the sentry_sdk.
However, do you have any suggestion on how to make the Middleware work with a gunicorn/uvicorn.workers.UvicornWorker?

What exact error do you get? I wonder which version gunicorn attempts to use

This is how I invoke the server:

gunicorn api:app -w 1 -k uvicorn.workers.UvicornWorker

So, using app = SentryAsgiMiddleware(app), I get:

File "/usr/lib/python3.7/inspect.py", line 2208, in _signature_from_callable
    raise TypeError('{!r} is not a callable object'.format(obj))

and the server won't start up at all.

If I add the _run_asgi2 or _run_asgi3 property access to the Middleware, the servers tarts, but on the first request, I get respectively

TypeError: __call__() missing 2 required positional arguments: 'receive' and 'send'

and

TypeError: _run_asgi3() missing 2 required positional arguments: 'receive' and 'send'

We're seeing this as well with the latest FastAPI + uvicorn:

sentry_sdk.init(dsn="https://<sentry_key_url>",
                    integrations=[AioHttpIntegration()],
                    release=f"<app_name>@<version>")
  • uvicorn[standard]==0.13.1
  • gunicorn==20.0.4
  • fastapi==0.62.0

I'm not a ASGI expert. I used to know WSGI. Is there a wrapper I can write to make the middleware callable without waiting for an upstream fix?

Hm. Okay here is a workaround that does not involve setting any server options:

class Asgi3(SentryAsgiMiddleware):
    async def __call__(self, *a, **kw):
        return await self._run_asgi3(*a, **kw)


app = Asgi3(create_app())

The problem really is that uvicorn tries to inspect method signatures too much to figure out what to call... it calls inspect.isfunction but _run_asgi3 is a method, so it returns false.

interesting. I'll mention this with the uvicorn devs. thanks for the help.

@kristjanvalur I don't think they can do much about it fwiw. It's just something we'll have to see through until asgi2 is dead.

Actually, the wrapper class above is problematic, because the parent class defines an instance _attribute_, __call__ that _should_ cause the _method_ to be hidden. That's what my linter says. but it fact it works.... somehow.
I'm using a simple wrapping closure instead:

def Wrap(middleware):
        async def wrapper(*args, **kwds):
            return await middleware(*args, **kwds)
        return wrapper

There is no need to explicitly call the _run_asgi3 method, signature detection of the middleware seems to work. It is only the code that is trying to analyze how to call the middleware (from uvicorn) that fails.

1 It fails to understand a __call__ instance attribute which is a bound method.
2 it fails to understand a bound method directly.

Not sure if we should continue this, but it could help to point out that the SentryAsgiMiddleware has an unusal way of being callable and that it might need to be wrapped like above in certain "smart" frameworks.

@kristjanvalur I don't think they can do much about it fwiw. It's just something we'll have to see through until asgi2 is dead.

Well, if there is a way for uvicorn.workers.Uvicornworker to somehow accept arguments. or have a subclass that just assumes protocol 3.

@kristjanvalur fair enough, so the fn body is actually irrelevant, yeah. I think the way forward is to drop asgi2 support, then the "unusual way" disappears

Hey folks,

Could anyone using the Sentry ASGI middleware install Uvicorn from https://github.com/encode/uvicorn/pull/914 as a quick confirmation that it fixes the problem seen here?

pip install "git+https://github.com/encode/uvicorn.git@fm/factory-inspection#egg=uvicorn"

@florimondmanca works for me!

Sweet. :)

To be clear AFAICT the bug here is not related to determining ASGI 2 vs ASGI 3, but related to "app instances" vs "app factories"

Are y'all sure about this? I don't see how the middleware exposes itself as app factory.

I don't see how the middleware exposes itself as app factory.

It doesn't, but due to the __call__ trick the code that Uvicorn was using to decide whether an app was a factory or not was failing. :-)

This is affecting Django users (me) as well as the above Sanic

@jesenbox Feel free to try the latest state as referred to in https://github.com/getsentry/sentry-python/issues/947#issuecomment-751349716 — should make it go away (you might have to install from master now, feel free to use a specific commit hash). We'll go ahead and issue a new release of Uvicorn with this patch soon.

Are you still experiencing this error with the latest version of uvicorn? If so, are we good to close this issue?

It's fixed, from my end.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

MaximZemskov picture MaximZemskov  Â·  3Comments

zegerius picture zegerius  Â·  4Comments

aklajnert picture aklajnert  Â·  6Comments

robd003 picture robd003  Â·  5Comments

amureki picture amureki  Â·  6Comments