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?
errors
, as soon as an (edit: incoercible) exception is raised.on_execute
and on_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:
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)
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)
0.220.0
Pay now to fund the work behind this issue.
Get updates on progress being made.
Maintainer is rewarded once the issue is completed.
You're funding impactful open source efforts
You want to contribute to this effort
You want to get funding like this too