Incremental delivery: experimental_execute_incrementally hangs (never terminates) on some invalid @stream queries
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 50/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Quiet
- Tech stack
- python
- Domain
- backend-api-design
Research direction
Start with the self-contained reproduction and run it repeatedly under graphql-core 3.3.0rc0 to observe the timeout. Trace experimental_execute_incrementally through the incremental-publisher/graph subsystem, especially _enqueue and _add_deferred_fragment_node. Done means the nested invalid @stream/@defer query terminates with an error or final hasNext: false without changing the behavior of the valid-query cases.
Written by the indexing model from the issue text.
Description
Hi there,
Whilst investigating a production bug (see #271), the fuzzing harness found a second bug. Not as critical as 271 because it only triggers on an invalid graphql shape, but it is a reproducible hang that can be reached in production. Found with the help of Claude code.
Hope this helps - unfortunately I don't have a suggested fix for this one as the root cause was less obvious.
- Ben (Mixcloud)
Summary
experimental_execute_incrementally never terminates for certain queries combining @stream with nested @defer/child selections — the async iterator over subsequent_results blocks forever, with no final hasNext: false. Reproduced reliably on graphql-core 3.3.0rc0 (≈13/15 attempts hang at a 3 s timeout in isolation, ≈9/10 at 5 s).
Scope, stated up front: every deadlocking query I found is spec-invalid — it fails graphql.validate (a streamed list field with no subselection, and/or a field-merge conflict between a streamed and a non-streamed selection of the same field). A caller that validates before executing rejects these first. Despite extensive search I could not find a spec-valid query that hangs — making the query valid removes the hang. So this is a robustness issue rather than a correctness issue on valid input: execution should fail fast / terminate rather than hang, since experimental_execute_incrementally can be (and is, for performance) called without a separate validation pass, and a hang on any input is a DoS risk.
Environment
- graphql-core
3.3.0rc0 - Python 3.14
Query (spec-invalid; hangs instead of erroring)
query {
obj {
... {
child {
child { ... @defer(label: "L4") { items @stream(initialCount: 1) } }
... @defer { child { fast items } }
}
}
}
}
validate() reports: "Fields 'child' conflict because subfields 'items' conflict because they have differing stream directives" and "Field 'items' … must have a selection of subfields". experimental_execute_incrementally neither raises nor completes.
What is / isn't required (isolation testing)
| Query | Valid? | Hangs? |
|---|---|---|
| The query above | invalid | yes (~13/15) |
items @stream(initialCount: 1) with no subselection, alone |
invalid | no |
Streamed + non-streamed items merge conflict, alone |
invalid | no |
| The same structure made valid (aliased, subselections added) | valid | no |
~15 other hand-crafted valid @stream/@defer/nesting shapes |
valid | no |
So neither invalid feature alone hangs; the deadlock needs the invalid feature embedded in the nested @defer + @stream + child structure — and making that structure valid stops it.
Notes
- Timing-sensitive (needs out-of-order async resolver completion), so the repro loops; not 100%/run but high (≈85–90%).
- Likely the same incremental-publisher/graph subsystem as the
_enqueue"race with a stopping consumer" guard and the separate_add_deferred_fragment_nodeinvariant crash (filed as a separate issue): the graph doesn't converge to a terminal state for these inputs.
Self-contained reproduction (graphql-core only)
import asyncio
import random
from graphql import (
GraphQLDeferDirective,
GraphQLField,
GraphQLList,
GraphQLObjectType,
GraphQLSchema,
GraphQLStreamDirective,
GraphQLString,
parse,
specified_directives,
validate,
)
from graphql.execution import (
ExperimentalIncrementalExecutionResults,
experimental_execute_incrementally,
)
from graphql.pyutils import is_awaitable
ATTEMPTS = 15
TIMEOUT_S = 3.0
async def r_fast(_s, _i) -> str:
await asyncio.sleep(random.uniform(0, 0.002))
return "fast"
async def r_obj(_s, _i) -> dict:
await asyncio.sleep(random.uniform(0, 0.001))
return {}
async def r_list(_s, _i) -> list:
await asyncio.sleep(random.uniform(0, 0.001))
return [{}, {}]
OBJ = GraphQLObjectType(
"Obj",
lambda: {
"fast": GraphQLField(GraphQLString, resolve=r_fast),
"child": GraphQLField(OBJ, resolve=r_obj),
"items": GraphQLField(GraphQLList(OBJ), resolve=r_list),
},
)
schema = GraphQLSchema(
GraphQLObjectType("Query", {"obj": GraphQLField(OBJ, resolve=r_obj)}),
directives=[*specified_directives, GraphQLDeferDirective, GraphQLStreamDirective],
)
QUERY = """
query {
obj {
... {
child {
child { ... @defer(label: "L4") { items @stream(initialCount: 1) } }
... @defer { child { fast items } }
}
}
}
}
"""
async def run_once() -> None:
result = experimental_execute_incrementally(schema, parse(QUERY), root_value={})
if is_awaitable(result):
result = await result
if isinstance(result, ExperimentalIncrementalExecutionResults):
async for _patch in result.subsequent_results:
pass
async def main() -> None:
import graphql
print("graphql-core:", graphql.__version__)
errs = validate(schema, parse(QUERY))
print("query is spec-" + ("INVALID" if errs else "VALID"))
hangs = 0
for i in range(1, ATTEMPTS + 1):
try:
await asyncio.wait_for(run_once(), TIMEOUT_S)
except TimeoutError:
hangs += 1
print(f"attempt {i}: HUNG (no termination within {TIMEOUT_S}s)")
print(f"{hangs}/{ATTEMPTS} attempts hung (never terminated).")
asyncio.run(main())
This non-termination, and the finding that valid queries do not reproduce it, were established by fuzzing @defer/@stream query shapes.
- Dominant language
- Python
- Stars
- 531
- Forks
- 147
- PR merge metrics
- No merged PRs in 30d
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from graphql-python/graphql-core
-
Difficulty 3/5 1-2 days Newbie friendliness 55/100
graphql-python/graphql-core#269 · 1 comment ·
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
graphql-python/graphql-core#267 · 1 comment ·
-
Difficulty 3/5 1-2 days Newbie friendliness 45/100
graphql-python/graphql-core#257 ·
-
Difficulty 5/5 Over a week Newbie friendliness 25/100
graphql-python/graphql-core#247 · 8 comments ·
-
Difficulty 3/5 1-2 days Newbie friendliness 35/100
graphql-python/graphql-core#223 · 1 comment ·
All issues in graphql-python/graphql-core
Similar issues
-
essnmx good first issue
Difficulty 1/5 Under an hour Newbie friendliness 95/100
-
[Feature] 奇物选择添加优先级 Open
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
syfoud/Simulated_Scepter#174 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Giskard-AI/giskard-oss#2840 · 1 comment ·
-
A claim comment carrying the issue number is silently declined while the workflow reports success Openarea: repo bug perceived difficulty: 2
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
yeti-platform/yeti#1380 ·