Repository: getsentry/sentry-python

Issue:
TypeError using SentryAsgiMiddleware with FastAPI/Starlette app
I cannot get `SentryAsgiMiddleware` to work with our FastAPI app. We tried to follow the example in the [Sentry docs](https://docs.sentry.io/platforms/python/asgi/), so the app module basically looks like this:

```python
from fastapi import FastAPI
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
...
app = FastAPI()

@app.post()
...

app = SentryAsgiMiddleware(app)
```

This gives an error on all requests, see the following stack trace:

```python
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/uvicorn/protocols/http/httptools_impl.py", line 385, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "/usr/local/lib/python3.7/site-packages/uvicorn/middleware/proxy_headers.py", line 45, in __call__
    return await self.app(scope, receive, send)
  File "/usr/local/lib/python3.7/site-packages/uvicorn/middleware/asgi2.py", line 7, in __call__
    await instance(receive, send)
  File "/usr/local/lib/python3.7/site-packages/sentry_sdk/integrations/asgi.py", line 54, in run_asgi2
    scope, lambda: self.app(scope)(receive, send)
  File "/usr/local/lib/python3.7/site-packages/sentry_sdk/integrations/asgi.py", line 93, in _run_app
    raise exc from None
  File "/usr/local/lib/python3.7/site-packages/sentry_sdk/integrations/asgi.py", line 90, in _run_app
    return await callback()
  File "/usr/local/lib/python3.7/site-packages/sentry_sdk/integrations/asgi.py", line 54, in <lambda>
    scope, lambda: self.app(scope)(receive, send)
TypeError: __call__() missing 2 required positional arguments: 'receive' and 'send'
```

Library versions:

- python==3.7.5
- sentry-sdk==0.13.2
- uvicorn==0.10.8
- fastapi==0.42.0
- starlette==0.12.9


Discussion:
It seems like uvicorn might inspect the function signature to figure out which ASGI version to use. Our function signature is `*args, **kwargs`, because we choose the ASGI version depending on the ASGI version we are called with. So uvicorn doesn't know how to call the app, but the app is wrapped with a middleware (Sentry) that doesn't know either.

I have a vague idea on how to fix this (middleware inspects app's function signature).

Could you try setting this option: https://www.uvicorn.org/settings/#application-interface (try all values except auto)
We run the server with `gunicorn` and `uvicorn.workers.UvicornWorker`, which makes it a bit inconvenient to change options in Uvicorn. We found a [suggestion](https://github.com/encode/uvicorn/issues/343) to subclass the `UvicornWorker` to change the config, will try that and report back.
The service is running now, after specifying ASGI3 interface using the following worker class:

```python
from uvicorn.workers import UvicornWorker


class GunicornUvicornWorker(UvicornWorker):
    CONFIG_KWARGS = {"loop": "uvloop", "http": "httptools", "interface": "asgi3"}
```
ok we will fix this in the sentry sdk in any case. thanks for reporting!

Resolving patch:
diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py
--- a/sentry_sdk/integrations/asgi.py
+++ b/sentry_sdk/integrations/asgi.py
@@ -4,7 +4,9 @@
 Based on Tom Christie's `sentry-asgi <https://github.com/encode/sentry-asgi>`_.
 """
 
+import asyncio
 import functools
+import inspect
 import urllib
 
 from sentry_sdk._types import MYPY
@@ -17,6 +19,7 @@
     from typing import Dict
     from typing import Any
     from typing import Optional
+    from typing import Callable
 
     from sentry_sdk._types import Event, Hint
 
@@ -37,26 +40,45 @@ def _capture_exception(hub, exc):
         hub.capture_event(event, hint=hint)
 
 
+def _looks_like_asgi3(app):
+    # type: (Any) -> bool
+    """
+    Try to figure out if an application object supports ASGI3.
+
+    This is how uvicorn figures out the application version as well.
+    """
+    if inspect.isclass(app):
+        return hasattr(app, "__await__")
+    elif inspect.isfunction(app):
+        return asyncio.iscoroutinefunction(app)
+    else:
+        call = getattr(app, "__call__", None)  # noqa
+        return asyncio.iscoroutinefunction(call)
+
+
 class SentryAsgiMiddleware:
-    __slots__ = ("app",)
+    __slots__ = ("app", "__call__")
 
     def __init__(self, app):
         # type: (Any) -> None
         self.app = app
 
-    def __call__(self, scope, receive=None, send=None):
-        # type: (Any, Any, Any) -> Any
-        if receive is None or send is None:
+        if _looks_like_asgi3(app):
+            self.__call__ = self._run_asgi3  # type: Callable[..., Any]
+        else:
+            self.__call__ = self._run_asgi2
 
-            async def run_asgi2(receive, send):
-                # type: (Any, Any) -> Any
-                return await self._run_app(
-                    scope, lambda: self.app(scope)(receive, send)
-                )
+    def _run_asgi2(self, scope):
+        # type: (Any) -> Any
+        async def inner(receive, send):
+            # type: (Any, Any) -> Any
+            return await self._run_app(scope, lambda: self.app(scope)(receive, send))
 
-            return run_asgi2
-        else:
-            return self._run_app(scope, lambda: self.app(scope, receive, send))
+        return inner
+
+    async def _run_asgi3(self, scope, receive, send):
+        # type: (Any, Any, Any) -> Any
+        return await self._run_app(scope, lambda: self.app(scope, receive, send))
 
     async def _run_app(self, scope, callback):
         # type: (Any, Any) -> Any


Test patch: