BlackSheep icon indicating copy to clipboard operation
BlackSheep copied to clipboard

Unable to integrate asgi3 middlewares

Open virajkanwade opened this issue 2 years ago • 5 comments

Describe the bug A description of what the bug is, possibly including how to reproduce it.

I am trying to include SentryAsgiMiddleware.

I have to hack it a little as per the known issue as:

class SentryAsgi3Middleware1(SentryAsgiMiddleware):
    """SentryAsgi3Middleware"""

    # pylint: disable=method-hidden
    async def __call__(self, *args, **kwargs):
        """Override __call__ slot"""
        return await self._run_asgi3(*args, **kwargs)

But the problem arrises when a URL is actually called.

TypeError: SentryAsgiMiddleware._run_asgi3() missing 1 required positional argument: 'send'

virajkanwade avatar May 24 '22 04:05 virajkanwade

Using https://github.com/encode/starlette/blob/b8ea367b4304a98653ec8ce9c794ad0ba6dcaf4b/starlette/middleware/base.py as reference, if I change the function to:

class SentryAsgi3Middleware(SentryAsgiMiddleware):
    """SentryAsgi3Middleware"""

    # pylint: disable=method-hidden
    async def __call__(self, scope: Request, receive: Callable[[Request], Awaitable[Response]], send: Callable[[Response], Awaitable[None]]) -> Response:
        """Override __call__ slot"""
        return await self._run_asgi3(*args, **kwargs)

I get AttributeError: 'SentryAsgi3Middleware' object has no attribute '__qualname__' which is AmbiguousMethodSignatureError

so somewhere normalize_middleware is failing to properly convert it.

virajkanwade avatar May 24 '22 04:05 virajkanwade

Hi @virajkanwade, Sorry for the late response. Yours is a very good question and this should have been documented explicitly. BlackSheep never had support for ASGI middlewares, because ASGI request scope is all dict, list, and scalar values, while BlackSheep always handled Request and Response classes, even before being made compatible with ASGI for the main request-response cycle.

I suspect that it won't be possible to add support for ASGI middlewares to BlackSheep, because it really needs to handle instances of Request and Response types, not dict. Adding support for a single middleware might be simple, but not so adding support for multiple ASGI middlewares in a row, for example, or mixed with BlackSheep specific middlewares.

RobertoPrevato avatar May 24 '22 05:05 RobertoPrevato

Hi @RobertoPrevato

Found a sample code for Starlette. Inside the sample asgi3 middleware def call:

❯ python app.py                                                                                                                                                                                                            ─╯
INFO:     Started server process [8701]
INFO:     Waiting for application startup.
  File "/private/tmp/timing-starlette-asgi-example/app.py", line 44, in <module>
    uvicorn.run(app)
  File "/private/tmp/timing-starlette-asgi-example/.venv/lib/python3.10/site-packages/uvicorn/main.py", line 463, in run
    server.run()
  File "/private/tmp/timing-starlette-asgi-example/.venv/lib/python3.10/site-packages/uvicorn/server.py", line 60, in run
    return asyncio.run(self.serve(sockets=sockets))
  File "/Users/vkanwade/.pyenv/versions/3.10.3/lib/python3.10/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "/private/tmp/timing-starlette-asgi-example/.venv/lib/python3.10/site-packages/uvicorn/lifespan/on.py", line 84, in main
    await app(scope, self.receive, self.send)
  File "/private/tmp/timing-starlette-asgi-example/.venv/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 75, in __call__
    return await self.app(scope, receive, send)
  File "/private/tmp/timing-starlette-asgi-example/.venv/lib/python3.10/site-packages/starlette/applications.py", line 133, in __call__
    await self.error_middleware(scope, receive, send)
  File "/private/tmp/timing-starlette-asgi-example/.venv/lib/python3.10/site-packages/starlette/middleware/errors.py", line 87, in __call__
    await self.app(scope, receive, send)
  File "/private/tmp/timing-starlette-asgi-example/.venv/lib/python3.10/site-packages/timing_asgi/middleware.py", line 31, in __call__
    traceback.print_stack()
{'type': 'lifespan', 'asgi': {'version': '3.0', 'spec_version': '2.0'}, 'app': <starlette.applications.Starlette object at 0x105d73d30>}
<bound method LifespanOn.receive of <uvicorn.lifespan.on.LifespanOn object at 0x106fb8f70>>
<bound method LifespanOn.send of <uvicorn.lifespan.on.LifespanOn object at 0x106fb8f70>>
2022-05-23 22:24:25 DEBUG [timing_asgi.middleware:46] ASGI scope of type lifespan is not supported yet
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
  File "/private/tmp/timing-starlette-asgi-example/app.py", line 44, in <module>
    uvicorn.run(app)
  File "/private/tmp/timing-starlette-asgi-example/.venv/lib/python3.10/site-packages/uvicorn/main.py", line 463, in run
    server.run()
  File "/private/tmp/timing-starlette-asgi-example/.venv/lib/python3.10/site-packages/uvicorn/server.py", line 60, in run
    return asyncio.run(self.serve(sockets=sockets))
  File "/Users/vkanwade/.pyenv/versions/3.10.3/lib/python3.10/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "/private/tmp/timing-starlette-asgi-example/.venv/lib/python3.10/site-packages/uvicorn/protocols/http/h11_impl.py", line 366, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "/private/tmp/timing-starlette-asgi-example/.venv/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 75, in __call__
    return await self.app(scope, receive, send)
  File "/private/tmp/timing-starlette-asgi-example/.venv/lib/python3.10/site-packages/starlette/applications.py", line 133, in __call__
    await self.error_middleware(scope, receive, send)
  File "/private/tmp/timing-starlette-asgi-example/.venv/lib/python3.10/site-packages/starlette/middleware/errors.py", line 100, in __call__
    await self.app(scope, receive, _send)
  File "/private/tmp/timing-starlette-asgi-example/.venv/lib/python3.10/site-packages/timing_asgi/middleware.py", line 31, in __call__
    traceback.print_stack()
{'type': 'http', 'asgi': {'version': '3.0', 'spec_version': '2.3'}, 'http_version': '1.1', 'server': ('127.0.0.1', 8000), 'client': ('127.0.0.1', 63904), 'scheme': 'http', 'method': 'GET', 'root_path': '', 'path': '/', 'raw_path': b'/', 'query_string': b'', 'headers': [(b'host', b'127.0.0.1:8000'), (b'user-agent', b'curl/7.79.1'), (b'accept', b'*/*')], 'app': <starlette.applications.Starlette object at 0x105d73d30>}
<bound method RequestResponseCycle.receive of <uvicorn.protocols.http.h11_impl.RequestResponseCycle object at 0x107029060>>
<function ServerErrorMiddleware.__call__.<locals>._send at 0x107050940>
INFO:     127.0.0.1:63904 - "GET / HTTP/1.1" 200 OK
^CINFO:     Shutting down
INFO:     Waiting for application shutdown.
INFO:     Application shutdown complete.
INFO:     Finished server process [8701]

Similar call stack for Blacksheep application call

❯ bin/run_dev                                                                                                                                                                                                              ─╯
INFO:     Will watch for changes in these directories: ['/private/tmp/blacksheep-app']
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [16277] using statreload
INFO:     Started server process [16289]
INFO:     Waiting for application startup.
  File "<string>", line 1, in <module>
  File "/Users/vkanwade/.pyenv/versions/3.10.3/lib/python3.10/multiprocessing/spawn.py", line 116, in spawn_main
    exitcode = _main(fd, parent_sentinel)
  File "/Users/vkanwade/.pyenv/versions/3.10.3/lib/python3.10/multiprocessing/spawn.py", line 129, in _main
    return self._bootstrap(parent_sentinel)
  File "/Users/vkanwade/.pyenv/versions/3.10.3/lib/python3.10/multiprocessing/process.py", line 315, in _bootstrap
    self.run()
  File "/Users/vkanwade/.pyenv/versions/3.10.3/lib/python3.10/multiprocessing/process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
  File "/Users/vkanwade/Library/Caches/pypoetry/virtualenvs/blacksheep-app/lib/python3.10/site-packages/uvicorn/subprocess.py", line 76, in subprocess_started
    target(sockets=sockets)
  File "/Users/vkanwade/Library/Caches/pypoetry/virtualenvs/blacksheep-app/lib/python3.10/site-packages/uvicorn/server.py", line 60, in run
    return asyncio.run(self.serve(sockets=sockets))
  File "/Users/vkanwade/.pyenv/versions/3.10.3/lib/python3.10/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "/Users/vkanwade/Library/Caches/pypoetry/virtualenvs/blacksheep-app/lib/python3.10/site-packages/uvicorn/lifespan/on.py", line 84, in main
    await app(scope, self.receive, self.send)
  File "/Users/vkanwade/Library/Caches/pypoetry/virtualenvs/blacksheep-app/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 75, in __call__
    return await self.app(scope, receive, send)
  File "/Users/vkanwade/Library/Caches/pypoetry/virtualenvs/blacksheep-app/lib/python3.10/site-packages/blacksheep/server/application.py", line 762, in __call__
    traceback.print_stack()
{'type': 'lifespan', 'asgi': {'version': '3.0', 'spec_version': '2.0'}}
<bound method LifespanOn.receive of <uvicorn.lifespan.on.LifespanOn object at 0x10c19e110>>
<bound method LifespanOn.send of <uvicorn.lifespan.on.LifespanOn object at 0x10c19e110>>
services {<class 'configuration.common.Configuration'>: <rodi.InstanceProvider object at 0x10e33df90>, 'Configuration': <rodi.InstanceProvider object at 0x10e33df90>, <class 'app.routes.organization.Organizations'>: <rodi.FactoryTypeProvider object at 0x10e33e200>, 'Organizations': <rodi.FactoryTypeProvider object at 0x10e33e200>, 'configuration': <rodi.InstanceProvider object at 0x10e33df90>, 'organizations': <rodi.FactoryTypeProvider object at 0x10e33e200>}
binders [<blacksheep.server.bindings.RequestBinder object at 0x10e33f010>, <blacksheep.server.bindings.QueryBinder object at 0x10e33f0a0>, <blacksheep.server.bindings.QueryBinder object at 0x10e33efe0>]
INFO:     Application startup complete.
  File "<string>", line 1, in <module>
  File "/Users/vkanwade/.pyenv/versions/3.10.3/lib/python3.10/multiprocessing/spawn.py", line 116, in spawn_main
    exitcode = _main(fd, parent_sentinel)
  File "/Users/vkanwade/.pyenv/versions/3.10.3/lib/python3.10/multiprocessing/spawn.py", line 129, in _main
    return self._bootstrap(parent_sentinel)
  File "/Users/vkanwade/.pyenv/versions/3.10.3/lib/python3.10/multiprocessing/process.py", line 315, in _bootstrap
    self.run()
  File "/Users/vkanwade/.pyenv/versions/3.10.3/lib/python3.10/multiprocessing/process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
  File "/Users/vkanwade/Library/Caches/pypoetry/virtualenvs/blacksheep-app/lib/python3.10/site-packages/uvicorn/subprocess.py", line 76, in subprocess_started
    target(sockets=sockets)
  File "/Users/vkanwade/Library/Caches/pypoetry/virtualenvs/blacksheep-app/lib/python3.10/site-packages/uvicorn/server.py", line 60, in run
    return asyncio.run(self.serve(sockets=sockets))
  File "/Users/vkanwade/.pyenv/versions/3.10.3/lib/python3.10/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "/Users/vkanwade/Library/Caches/pypoetry/virtualenvs/blacksheep-app/lib/python3.10/site-packages/uvicorn/protocols/http/httptools_impl.py", line 372, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "/Users/vkanwade/Library/Caches/pypoetry/virtualenvs/blacksheep-app/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 75, in __call__
    return await self.app(scope, receive, send)
  File "/Users/vkanwade/Library/Caches/pypoetry/virtualenvs/blacksheep-app/lib/python3.10/site-packages/blacksheep/server/application.py", line 762, in __call__
    traceback.print_stack()
{'type': 'http', 'asgi': {'version': '3.0', 'spec_version': '2.3'}, 'http_version': '1.1', 'server': ('127.0.0.1', 8000), 'client': ('127.0.0.1', 64658), 'scheme': 'http', 'root_path': '', 'headers': [(b'host', b'localhost:8000'), (b'user-agent', b'curl/7.79.1'), (b'accept', b'*/*')], 'method': 'GET', 'path': '/health', 'raw_path': b'/health', 'query_string': b''}
<bound method RequestResponseCycle.receive of <uvicorn.protocols.http.httptools_impl.RequestResponseCycle object at 0x10e3a1c60>>
<bound method RequestResponseCycle.send of <uvicorn.protocols.http.httptools_impl.RequestResponseCycle object at 0x10e3a1c60>>
INFO:     127.0.0.1:64658 - "GET /health HTTP/1.1" 200 OK
^CINFO:     Shutting down
INFO:     Waiting for application shutdown.
INFO:     Application shutdown complete.
INFO:     Finished server process [16289]
INFO:     Stopping reloader process [16277]

virajkanwade avatar May 24 '22 14:05 virajkanwade

key areas of interest

starlette:

  File "/private/tmp/timing-starlette-asgi-example/.venv/lib/python3.10/site-packages/uvicorn/lifespan/on.py", line 84, in main
    await app(scope, self.receive, self.send)
  File "/private/tmp/timing-starlette-asgi-example/.venv/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 75, in __call__
    return await self.app(scope, receive, send)
  File "/private/tmp/timing-starlette-asgi-example/.venv/lib/python3.10/site-packages/starlette/applications.py", line 133, in __call__
    await self.error_middleware(scope, receive, send)

{'type': 'lifespan', 'asgi': {'version': '3.0', 'spec_version': '2.0'}, 'app': <starlette.applications.Starlette object at 0x105d73d30>}
<bound method LifespanOn.receive of <uvicorn.lifespan.on.LifespanOn object at 0x106fb8f70>>
<bound method LifespanOn.send of <uvicorn.lifespan.on.LifespanOn object at 0x106fb8f70>>

blacksheep:

 File "/Users/vkanwade/Library/Caches/pypoetry/virtualenvs/blacksheep-app/lib/python3.10/site-packages/uvicorn/lifespan/on.py", line 84, in main
    await app(scope, self.receive, self.send)
  File "/Users/vkanwade/Library/Caches/pypoetry/virtualenvs/blacksheep-app/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 75, in __call__
    return await self.app(scope, receive, send)
  File "/Users/vkanwade/Library/Caches/pypoetry/virtualenvs/blacksheep-app/lib/python3.10/site-packages/blacksheep/server/application.py", line 762, in __call__
    traceback.print_stack()
{'type': 'lifespan', 'asgi': {'version': '3.0', 'spec_version': '2.0'}}
<bound method LifespanOn.receive of <uvicorn.lifespan.on.LifespanOn object at 0x10c19e110>>
<bound method LifespanOn.send of <uvicorn.lifespan.on.LifespanOn object at 0x10c19e110>>

As you can see, blacksheep too gets the same scope, receive and send as starlette in application.call. So I guess we just need to identify asgi3 middleware and somehow pass through the receive and send to it?

virajkanwade avatar May 24 '22 14:05 virajkanwade

starlette:

  File "/private/tmp/timing-starlette-asgi-example/.venv/lib/python3.10/site-packages/uvicorn/protocols/http/h11_impl.py", line 366, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "/private/tmp/timing-starlette-asgi-example/.venv/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 75, in __call__
    return await self.app(scope, receive, send)
  File "/private/tmp/timing-starlette-asgi-example/.venv/lib/python3.10/site-packages/starlette/applications.py", line 133, in __call__
    await self.error_middleware(scope, receive, send)

{'type': 'http', 'asgi': {'version': '3.0', 'spec_version': '2.3'}, 'http_version': '1.1', 'server': ('127.0.0.1', 8000), 'client': ('127.0.0.1', 63904), 'scheme': 'http', 'method': 'GET', 'root_path': '', 'path': '/', 'raw_path': b'/', 'query_string': b'', 'headers': [(b'host', b'127.0.0.1:8000'), (b'user-agent', b'curl/7.79.1'), (b'accept', b'*/*')], 'app': <starlette.applications.Starlette object at 0x105d73d30>}
<bound method RequestResponseCycle.receive of <uvicorn.protocols.http.h11_impl.RequestResponseCycle object at 0x107029060>>
<function ServerErrorMiddleware.__call__.<locals>._send at 0x107050940>

blacksheep:

  File "/Users/vkanwade/Library/Caches/pypoetry/virtualenvs/blacksheep-app/lib/python3.10/site-packages/uvicorn/protocols/http/httptools_impl.py", line 372, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "/Users/vkanwade/Library/Caches/pypoetry/virtualenvs/blacksheep-app/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 75, in __call__
    return await self.app(scope, receive, send)
  File "/Users/vkanwade/Library/Caches/pypoetry/virtualenvs/blacksheep-app/lib/python3.10/site-packages/blacksheep/server/application.py", line 762, in __call__
    traceback.print_stack()
{'type': 'http', 'asgi': {'version': '3.0', 'spec_version': '2.3'}, 'http_version': '1.1', 'server': ('127.0.0.1', 8000), 'client': ('127.0.0.1', 64658), 'scheme': 'http', 'root_path': '', 'headers': [(b'host', b'localhost:8000'), (b'user-agent', b'curl/7.79.1'), (b'accept', b'*/*')], 'method': 'GET', 'path': '/health', 'raw_path': b'/health', 'query_string': b''}
<bound method RequestResponseCycle.receive of <uvicorn.protocols.http.httptools_impl.RequestResponseCycle object at 0x10e3a1c60>>
<bound method RequestResponseCycle.send of <uvicorn.protocols.http.httptools_impl.RequestResponseCycle object at 0x10e3a1c60>>

Here too scope is similar in both. receive is same. Starlette seems to be overriding send.

virajkanwade avatar May 24 '22 14:05 virajkanwade