-
-
Notifications
You must be signed in to change notification settings - Fork 577
Description
TL;DR
Strawberry short-circuits the HTTP response whenever there is an uncaught exception. This reduces the latency, but leads to:
(i) (a) incomplete and (b) nondeterministic responses (edit: established in the comments that it's expected)
(ii) hooks being completed before some resolvers, leading to apparent violation of a contract
I wonder if it would be possible to make Strawberry run all resolves to the end, even if some of them raise uncaught exceptions?
Describe the Bug
- Strawberry executes all resolvers, even if there was an uncaught exception which triggered an early HTTP response with errors.
- However, it eagerly returns a response with
errors
, as soon as an (edit: incoercible) exception is raised. - Finally, it completes all lifecycle hooks before return the response – including
on_execute
andon_operation
.
This last point can lead to issues – it violates the invariant that on_execute
/ on_operation
lifecycle hooks wrap around all resolver executions.
This can be problematic when these hooks do state management, like in the example given in Strawberry's docs. As a result, in addition to seeing the original uncaught exception in our observability suite, we have additional noise from knock-on failures – caused by premature completion of hooks.
Is this natural behaviour given how various async tasks are orchestrated, or is possible to tweak this a little? I'm thinking:
- cancelling the tasks that won't be used for the response anyway (as it's been already returned)
- waiting until all resolvers finish to return the response
In fact, 2 may have other benefits – making the responses more (a) complete and (b) predictable. Currently, the GraphQL responses (i.e. which fields will return data and which won't) are non-deterministic (albeit a little faster thanks to the uncaught exception short-circuit). (edit: established in the comments that the short-circuiting is expected)
Repro code
Schema:
@strawberry.type
class Query:
@strawberry.field
@staticmethod
async def fail() -> str:
await sleep(0.5)
raise Exception(f"'fail' resolver has failed ({datetime.now()})")
@strawberry.field
@staticmethod
async def wait() -> str:
await sleep(2)
print(f"'wait' resolver is about to return ({datetime.now()})")
return "foo"
Logging extension:
class MyCustomExtension(SchemaExtension):
@override
def on_execute(self) -> Generator[None, None, None]:
print(f"'on_execute' start ({datetime.now()})")
yield
print(f"'on_execute' end ({datetime.now()})")
@override
async def resolve(
self,
_next: Callable[..., object],
root: object,
info: GraphQLResolveInfo,
*args,
**kwargs,
) -> AwaitableOrValue[object]:
print(f"'{info.field_name}' resolver start ({datetime.now()})")
result = await await_maybe(_next(root, info, *args, **kwargs))
print(f"'{info.field_name}' resolver end ({datetime.now()})")
return result
Example query:
query {
fail
wait
}
Example response:
{
"data": null,
"errors": [
{
"message": "'fail' resolver has failed (2024-03-19 21:08:12.088337)",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"fail"
]
}
]
}
Logs demonstrating that the resolvers continue being executed after hooks complete:
'on_execute' start (2024-03-19 21:08:11.587192)
'fail' resolver start (2024-03-19 21:08:11.587345)
'wait' resolver start (2024-03-19 21:08:11.587378)
'fail' resolver has failed (2024-03-19 21:08:12.088337)
GraphQL request:2:3
1 | query {
2 | fail
| ^
3 | wait
Traceback (most recent call last):
File "/Users/kkom/Repos/isometric/python/services/backend/.venv/lib/python3.12/site-packages/graphql/execution/execute.py", line 528, in await_result
return_type, field_nodes, info, path, await result
^^^^^^^^^^^^
File "/Users/kkom/Repos/isometric/python/services/backend/backend/api/graphql/extensions/extensions.py", line 30, in resolve
result = await await_maybe(_next(root, info, *args, **kwargs))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/kkom/Repos/isometric/python/services/backend/.venv/lib/python3.12/site-packages/strawberry/utils/await_maybe.py", line 12, in await_maybe
return await value
^^^^^^^^^^^
File "/Users/kkom/Repos/isometric/python/services/backend/.venv/lib/python3.12/site-packages/strawberry/schema/schema_converter.py", line 682, in _async_resolver
return await await_maybe(
^^^^^^^^^^^^^^^^^^
File "/Users/kkom/Repos/isometric/python/services/backend/.venv/lib/python3.12/site-packages/strawberry/utils/await_maybe.py", line 12, in await_maybe
return await value
^^^^^^^^^^^
File "/Users/kkom/Repos/isometric/python/services/backend/backend/api/graphql/schemas/public.py", line 55, in fail
raise Exception(f"'fail' resolver has failed ({datetime.now()})")
Exception: 'fail' resolver has failed (2024-03-19 21:08:12.088337)
'on_execute' end (2024-03-19 21:08:12.096968)
INFO: 127.0.0.1:58138 - "POST /graphql HTTP/1.1" 200 OK
'wait' resolver is about to return (2024-03-19 21:08:13.588281)
'wait' resolver end (2024-03-19 21:08:13.588422)
System Information
- Strawberry version (if applicable):
0.220.0