Uvicorn: uvirorn.workers.UvicornWorker fails with certain ASGI middleware

Created on 18 Dec 2020  路  7Comments  路  Source: encode/uvicorn

Description

Certain middleware is fails, either when launcing Uvicornworker, or when calling the ASGI handler.
A concrete case is the SentryAsgiMiddleware.
See https://github.com/getsentry/sentry-python/issues/947 for info.
Not sure if the problem is in Uvicorn or Sentry.

To reproduce

Install the Sentry lib in a ASGI application, run with gunicorn/uviworker

from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
app = SentryAsgiMiddleware(app)

and

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

Expected behavior

Application launches and runs

Actual behavior

As set up above, the following error is produced when launcing gunicorn:

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

If I forcefully expose either a asgi2 or asgi3 _interface_ from the middleware, using either the _run_asgi2() or _run_asgi3() methods on the middleware object, the servers starts up, but then fails when handling a request, with e.g.:

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

Apparently, the introspection of the callable fails to recognize _method_ objects.

Additional context

Using the uvicorn webserver it is possible to specify asgi2/3 interface using a commandline argument, and thus override the detection mechanism, which seems to be failing on this particular middleware. Using the Uvicorn worker this appears to be not possible.

again, see issue here. See https://github.com/getsentry/sentry-python/issues/947 for info.

bug

All 7 comments

Perhaps having a subclass of UvicornWorker that just assumes asgi3 and doesn't try to second-guess the protocol from the callable. Or alternatively, make UvicornWorker asgi3 only and have a UvicornWorkerAsgi2 class for legacy apps?
There is already something called UnvicornH11Worker..

this seems more like an issue introduced in #875

Welp, indeed, SentryAsgiMiddleware is doing a weird thing by assigning a __call__ method dynamically, and so Python doesn't view that as a "callable" (which is weird considering that "callable" _should_ be a synonym for "has a __call__ method"?).

https://github.com/getsentry/sentry-python/blob/9f1aeb387410ead07430becb7f6e6aa8cd80b205/sentry_sdk/integrations/asgi.py#L91-L94

>>> class Middleware:
...     def __init__(self):
...         self.__call__ = self._run
...     def _run(self): pass
...
>>> m = Middleware()
>>> import inspect
>>> inspect.signature(m)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/florimond/.pyenv/versions/3.9.0/lib/python3.9/inspect.py", line 3118, in signature
    return Signature.from_callable(obj, follow_wrapped=follow_wrapped)
  File "/Users/florimond/.pyenv/versions/3.9.0/lib/python3.9/inspect.py", line 2867, in from_callable
    return _signature_from_callable(obj, sigcls=cls,
  File "/Users/florimond/.pyenv/versions/3.9.0/lib/python3.9/inspect.py", line 2242, in _signature_from_callable
    raise TypeError('{!r} is not a callable object'.format(obj))
TypeError: <__main__.Middleware object at 0x10f4dbb20> is not a callable object

Well, I believe in duck typing more than anything. Why not simply try to call the target and see what happens, rather than to try to rely on the always lying metadata?

Anyway, the momentum is towards abandoning asgi2. It would be great to be able to instruct uvicornworker to just assume V3 and get on with it

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". The latter case was introduced recently via a --factory flag, and the code at fault was meant to try and detect when a factory is passed without the flag (so we don't just crash with an odd "app expected 0 arguments but got 3" error message).

Hm, being an old school Python guy, I probably would have opted for just calling the object, without an argument, and if that worked, assume that it was a factory. Otherwise, handle the exception and assume it was an app instance. In my experience type inspection only ever caused grief.

Was this page helpful?
0 / 5 - 0 ratings