Repository: getsentry/sentry-python

Issue:
Strawberry integration crashes if no query is in request
### How do you use Sentry?

Sentry Saas (sentry.io)

### Version

1.40.2

### Steps to Reproduce

Running an app with the StrawberryIntegration crashes if graphql endpoint is called with no `query` field in the input data.

1. Place the attached litestar app code in `app.py` and install the requirements (sentry, strawberry graphql and litestar)
2. Run the local server using `litestar run`
3. Call the graphql endpoint using `curl -v -X POST -d "{}" -H "Content-Type: application/json" http://localhost:8000/graphql`
4. This causes an exception to be raised, whereas if I comment out the sentry_sdk.init() line in the code, the "expected" result of a 400 Bad Request is returned. 
5. (The additional unwanted effect is that the 500 exception goes to sentry as an error in a production app - this is just a minimal reproduction).

app.py:

```
import logging

from litestar import Litestar
from litestar.logging import LoggingConfig

import sentry_sdk
from sentry_sdk.integrations.strawberry import StrawberryIntegration

import strawberry
from strawberry.litestar import make_graphql_controller

sentry_sdk.init(integrations=[StrawberryIntegration(async_execution=True)])


@strawberry.type
class Query:
    @strawberry.field
    def hello(self) -> str:
        return "Hello World"


class Schema(strawberry.Schema):
    async def execute(self, *args, **kwargs):
        try:
            return await super().execute(*args, **kwargs)
        except Exception as e:
            logging.getLogger("root").exception(e)
            raise


schema = Schema(Query)

GraphQLController = make_graphql_controller(schema, path="/graphql", debug=True)

app = Litestar(
    route_handlers=[GraphQLController],
    logging_config=LoggingConfig(
        root={"level": logging.getLevelName(logging.INFO), "handlers": ["console"]},
        formatters={
            "standard": {
                "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
            }
        },
    ),
)
```

pip freeze:

```
anyio==4.2.0
certifi==2024.2.2
click==8.1.7
EditorConfig==0.12.3
Faker==23.0.0
fast-query-parsers==1.0.3
graphql-core==3.2.3
h11==0.14.0
httpcore==1.0.2
httptools==0.6.1
httpx==0.26.0
idna==3.6
Jinja2==3.1.3
jsbeautifier==1.14.11
litestar==2.6.0
markdown-it-py==3.0.0
MarkupSafe==2.1.5
mdurl==0.1.2
msgspec==0.18.6
multidict==6.0.5
polyfactory==2.14.1
Pygments==2.17.2
python-dateutil==2.8.2
python-dotenv==1.0.1
PyYAML==6.0.1
rich==13.7.0
rich-click==1.7.3
sentry-sdk==1.40.2
six==1.16.0
sniffio==1.3.0
strawberry-graphql==0.219.1
typing_extensions==4.9.0
urllib3==2.2.0
uvicorn==0.27.0.post1
uvloop==0.19.0
watchfiles==0.21.0
websockets==12.0
```


### Expected Result

```
< HTTP/1.1 400 Bad Request
< date: Wed, 07 Feb 2024 13:52:14 GMT
< server: uvicorn
< content-type: text/plain; charset=utf-8
< content-length: 37
< 
* Connection #0 to host localhost left intact
No GraphQL query found in the request⏎    
```


### Actual Result

```
< HTTP/1.1 500 Internal Server Error
< date: Wed, 07 Feb 2024 13:53:11 GMT
< server: uvicorn
< content-type: application/json
< content-length: 52
< 
* Connection #0 to host localhost left intact
{"status_code":500,"detail":"Internal Server Error"}⏎ 
```

On the server console, there is a traceback from inside the strawberry integration:

```
2024-02-07 13:53:11,970 - root - ERROR - 'NoneType' object has no attribute 'strip'
Traceback (most recent call last):
  File "/Users/james/repos/sentrystrawberry/app.py", line 25, in execute
    return await super().execute(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/james/.pyenv/versions/3.12.0/envs/mixcloud/lib/python3.12/site-packages/strawberry/schema/schema.py", line 263, in execute
    result = await execute(
             ^^^^^^^^^^^^^^
  File "/Users/james/.pyenv/versions/3.12.0/envs/mixcloud/lib/python3.12/site-packages/sentry_sdk/integrations/strawberry.py", line 266, in _sentry_patched_execute_async
    result = await old_execute_async(*args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/james/.pyenv/versions/3.12.0/envs/mixcloud/lib/python3.12/site-packages/strawberry/schema/execute.py", line 91, in execute
    async with extensions_runner.operation():
  File "/Users/james/.pyenv/versions/3.12.0/envs/mixcloud/lib/python3.12/site-packages/strawberry/extensions/context.py", line 191, in __aenter__
    await self.run_hooks_async()
  File "/Users/james/.pyenv/versions/3.12.0/envs/mixcloud/lib/python3.12/site-packages/strawberry/extensions/context.py", line 177, in run_hooks_async
    hook.initialized_hook.__next__()  # type: ignore[union-attr]
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/james/.pyenv/versions/3.12.0/envs/mixcloud/lib/python3.12/site-packages/sentry_sdk/integrations/strawberry.py", line 148, in on_operation
    if self.execution_context.query.strip().startswith("mutation"):
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'strip'
```
Strawberry integration crashes if no query is in request
### How do you use Sentry?

Sentry Saas (sentry.io)

### Version

1.40.2

### Steps to Reproduce

Running an app with the StrawberryIntegration crashes if graphql endpoint is called with no `query` field in the input data.

1. Place the attached litestar app code in `app.py` and install the requirements (sentry, strawberry graphql and litestar)
2. Run the local server using `litestar run`
3. Call the graphql endpoint using `curl -v -X POST -d "{}" -H "Content-Type: application/json" http://localhost:8000/graphql`
4. This causes an exception to be raised, whereas if I comment out the sentry_sdk.init() line in the code, the "expected" result of a 400 Bad Request is returned. 
5. (The additional unwanted effect is that the 500 exception goes to sentry as an error in a production app - this is just a minimal reproduction).

app.py:

```
import logging

from litestar import Litestar
from litestar.logging import LoggingConfig

import sentry_sdk
from sentry_sdk.integrations.strawberry import StrawberryIntegration

import strawberry
from strawberry.litestar import make_graphql_controller

sentry_sdk.init(integrations=[StrawberryIntegration(async_execution=True)])


@strawberry.type
class Query:
    @strawberry.field
    def hello(self) -> str:
        return "Hello World"


class Schema(strawberry.Schema):
    async def execute(self, *args, **kwargs):
        try:
            return await super().execute(*args, **kwargs)
        except Exception as e:
            logging.getLogger("root").exception(e)
            raise


schema = Schema(Query)

GraphQLController = make_graphql_controller(schema, path="/graphql", debug=True)

app = Litestar(
    route_handlers=[GraphQLController],
    logging_config=LoggingConfig(
        root={"level": logging.getLevelName(logging.INFO), "handlers": ["console"]},
        formatters={
            "standard": {
                "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
            }
        },
    ),
)
```

pip freeze:

```
anyio==4.2.0
certifi==2024.2.2
click==8.1.7
EditorConfig==0.12.3
Faker==23.0.0
fast-query-parsers==1.0.3
graphql-core==3.2.3
h11==0.14.0
httpcore==1.0.2
httptools==0.6.1
httpx==0.26.0
idna==3.6
Jinja2==3.1.3
jsbeautifier==1.14.11
litestar==2.6.0
markdown-it-py==3.0.0
MarkupSafe==2.1.5
mdurl==0.1.2
msgspec==0.18.6
multidict==6.0.5
polyfactory==2.14.1
Pygments==2.17.2
python-dateutil==2.8.2
python-dotenv==1.0.1
PyYAML==6.0.1
rich==13.7.0
rich-click==1.7.3
sentry-sdk==1.40.2
six==1.16.0
sniffio==1.3.0
strawberry-graphql==0.219.1
typing_extensions==4.9.0
urllib3==2.2.0
uvicorn==0.27.0.post1
uvloop==0.19.0
watchfiles==0.21.0
websockets==12.0
```


### Expected Result

```
< HTTP/1.1 400 Bad Request
< date: Wed, 07 Feb 2024 13:52:14 GMT
< server: uvicorn
< content-type: text/plain; charset=utf-8
< content-length: 37
< 
* Connection #0 to host localhost left intact
No GraphQL query found in the request⏎    
```


### Actual Result

```
< HTTP/1.1 500 Internal Server Error
< date: Wed, 07 Feb 2024 13:53:11 GMT
< server: uvicorn
< content-type: application/json
< content-length: 52
< 
* Connection #0 to host localhost left intact
{"status_code":500,"detail":"Internal Server Error"}⏎ 
```

On the server console, there is a traceback from inside the strawberry integration:

```
2024-02-07 13:53:11,970 - root - ERROR - 'NoneType' object has no attribute 'strip'
Traceback (most recent call last):
  File "/Users/james/repos/sentrystrawberry/app.py", line 25, in execute
    return await super().execute(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/james/.pyenv/versions/3.12.0/envs/mixcloud/lib/python3.12/site-packages/strawberry/schema/schema.py", line 263, in execute
    result = await execute(
             ^^^^^^^^^^^^^^
  File "/Users/james/.pyenv/versions/3.12.0/envs/mixcloud/lib/python3.12/site-packages/sentry_sdk/integrations/strawberry.py", line 266, in _sentry_patched_execute_async
    result = await old_execute_async(*args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/james/.pyenv/versions/3.12.0/envs/mixcloud/lib/python3.12/site-packages/strawberry/schema/execute.py", line 91, in execute
    async with extensions_runner.operation():
  File "/Users/james/.pyenv/versions/3.12.0/envs/mixcloud/lib/python3.12/site-packages/strawberry/extensions/context.py", line 191, in __aenter__
    await self.run_hooks_async()
  File "/Users/james/.pyenv/versions/3.12.0/envs/mixcloud/lib/python3.12/site-packages/strawberry/extensions/context.py", line 177, in run_hooks_async
    hook.initialized_hook.__next__()  # type: ignore[union-attr]
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/james/.pyenv/versions/3.12.0/envs/mixcloud/lib/python3.12/site-packages/sentry_sdk/integrations/strawberry.py", line 148, in on_operation
    if self.execution_context.query.strip().startswith("mutation"):
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'strip'
```


Discussion:
Thanks for the super nice bug report @jthorniley! Should be easy enough for us to fix -- we need to guard against `self.execution_context.query` being `None` in a couple places.
Thanks for the super nice bug report @jthorniley! Should be easy enough for us to fix -- we need to guard against `self.execution_context.query` being `None` in a couple places.

Resolving patch:
diff --git a/sentry_sdk/integrations/strawberry.py b/sentry_sdk/integrations/strawberry.py
--- a/sentry_sdk/integrations/strawberry.py
+++ b/sentry_sdk/integrations/strawberry.py
@@ -145,6 +145,9 @@ def on_operation(self):
         operation_type = "query"
         op = OP.GRAPHQL_QUERY
 
+        if self.execution_context.query is None:
+            self.execution_context.query = ""
+
         if self.execution_context.query.strip().startswith("mutation"):
             operation_type = "mutation"
             op = OP.GRAPHQL_MUTATION


Test patch:
diff --git a/tests/integrations/strawberry/test_strawberry_py3.py b/tests/integrations/strawberry/test_strawberry_py3.py
--- a/tests/integrations/strawberry/test_strawberry_py3.py
+++ b/tests/integrations/strawberry/test_strawberry_py3.py
@@ -600,3 +600,30 @@ def test_transaction_mutation(
             "graphql.path": "change",
         }
     )
+
+
+@parameterize_strawberry_test
+def test_handle_none_query_gracefully(
+    request,
+    sentry_init,
+    capture_events,
+    client_factory,
+    async_execution,
+    framework_integrations,
+):
+    sentry_init(
+        integrations=[
+            StrawberryIntegration(async_execution=async_execution),
+        ]
+        + framework_integrations,
+    )
+    events = capture_events()
+
+    schema = strawberry.Schema(Query)
+
+    client_factory = request.getfixturevalue(client_factory)
+    client = client_factory(schema)
+
+    client.post("/graphql", json={})
+
+    assert len(events) == 0, "expected no events to be sent to Sentry"