@@ -3163,3 +3163,78 @@ async def author(_info):
31633163 with pytest .raises (AbortError , match = "This operation was aborted" ):
31643164 await anext_task
31653165 assert items_source .aclose_finished
3166+
3167+
3168+ def describe_defer_directive_with_errors_and_nested_defer ():
3169+ """Regression tests for graphql-python/graphql-core#271."""
3170+
3171+ async def does_not_reach_invalid_state_when_sibling_fragments_error ():
3172+ """Sibling erroring ``@defer`` fragments with nested ``@defer`` must not crash.
3173+
3174+ Two sibling top-level ``@defer`` fragments that each resolve a distinct
3175+ erroring field and each contain a nested ``@defer`` used to crash the
3176+ incremental graph with "Invalid state while adding deferred fragment
3177+ node": a failing
3178+ execution group removes its parentless (root) deferred fragment, and a
3179+ sibling execution group completing afterwards then recursed up into the
3180+ removed record and hit the invariant. See issue #271.
3181+ """
3182+
3183+ async def resolve_fast (_source , _info ) -> str :
3184+ await sleep (0 )
3185+ return "fast"
3186+
3187+ def resolve_error (_source , _info ) -> str :
3188+ raise RuntimeError ("bad" )
3189+
3190+ async def resolve_obj (_source , _info ) -> dict :
3191+ await sleep (0 )
3192+ return {}
3193+
3194+ obj_type : GraphQLObjectType = GraphQLObjectType (
3195+ "Obj" ,
3196+ lambda : {
3197+ "fast" : GraphQLField (GraphQLString , resolve = resolve_fast ),
3198+ "boom" : GraphQLField (GraphQLString , resolve = resolve_error ),
3199+ "boomNonNull" : GraphQLField (
3200+ GraphQLNonNull (GraphQLString ), resolve = resolve_error
3201+ ),
3202+ "child" : GraphQLField (obj_type , resolve = resolve_obj ),
3203+ },
3204+ )
3205+ local_schema = GraphQLSchema (
3206+ GraphQLObjectType (
3207+ "Query" , {"obj" : GraphQLField (obj_type , resolve = resolve_obj )}
3208+ )
3209+ )
3210+ # The two siblings must error on DISTINCT fields (`boom` vs `boomNonNull`):
3211+ # erroring on the same field in both does not reproduce the crash. The
3212+ # nested `@defer` selections may be identical.
3213+ document = parse (
3214+ """
3215+ query {
3216+ obj {
3217+ ... @defer(label: "a") {
3218+ boom
3219+ child { ... @defer(label: "b") { fast } }
3220+ }
3221+ ... @defer(label: "c") {
3222+ boomNonNull
3223+ child { ... @defer(label: "d") { fast } }
3224+ }
3225+ }
3226+ }
3227+ """
3228+ )
3229+
3230+ for early_execution in (False , True ):
3231+ result = experimental_execute_incrementally (
3232+ local_schema , document , {}, enable_early_execution = early_execution
3233+ )
3234+ assert is_awaitable (result )
3235+ result = await result
3236+ assert isinstance (result , ExperimentalIncrementalExecutionResults )
3237+ # Draining the stream must terminate without raising
3238+ # "Invalid state while adding deferred fragment node".
3239+ async for _patch in result .subsequent_results :
3240+ pass
0 commit comments