Summary
Relay’s @defer transform namespaces label as {owningDefinition}$defer${label} and checks uniqueness while walking each fragment/operation definition once. Persist then inlines fragments. Spreading that same owning fragment on two live paths (typically two @alias parents) prints the compiled label twice in the persisted operation.
That persisted document violates Defer And Stream Directive Labels Are Unique. The compiler still succeeds. The failure shows up only when a GraphQL gateway validates the inlined operation.
This is the same class of bug as #5270 (argument-variant clones keep the original fragment’s compiled label). Here the second copy comes from @alias blocking selection merge, not from @arguments variants.
Reproduction
fragment ButtonsFragment on Query {
__typename
}
fragment ChildFragment on Query {
...ButtonsFragment@defer(label: "Buttons")
}
fragment HeadingFragment on Query {
...ChildFragment
}
fragment ListFragment on Query {
...ChildFragment
}
query ParentQuery {
...HeadingFragment @alias
...ListFragment @alias
}
- Compile with persist enabled.
- Inspect the persisted operation text for
ParentQuery.
Actual
The compiler is green. Persist inlines ChildFragment onto both aliased parents, so the operation contains the same compiled label twice, e.g.
@defer(label: "ChildFragment$defer$Buttons")
(or _ChildFragment$defer$Buttons, depending on compiler version).
Without @alias, Relay merges the two ChildFragment spreads and persist emits the label once.
Expected
One of:
- Fail compile with
LabelNotUniqueForDeferStream for this operation, or
- Namespace / re-unique the compiled label per inlined site (e.g. include the
@alias path or variant name), so persist cannot emit the same identity twice.
Why this started 500ing in production
Nothing in the client GraphQL source changed. Federation / Apollo Router began enforcing label uniqueness on the operation the planner expands — the persisted, inlined document Relay already shipped.
Apollo Router v2.10.5 and v2.16.1 release notes: “Update operation validation to enforce unique @defer labels.” That was a query-planner DoS fix (duplicate nested @defer labels could unbounded-recurse; GHSA-gr6h-4wpf-xp52 / AIKIDO-2026-807777). After those patches, a previously accepted persisted query is rejected.
Client libraries do not catch this before persist:
- relay-compiler uniqueness in
defer_stream.rs (record_label + transform_label) runs per definition. ChildFragment is visited once, so ChildFragment$defer$TradeButtons is recorded once. Persist inlining happens later.
- relay-runtime sends a persisted query id; it does not validate operation text.
- graphql-js 16
specifiedRules has no DeferStreamDirectiveLabelRule.
- graphql-js 17’s rule walks
Directive nodes in the AST it is given and does not expand spreads. Validating source still passes (one @defer in ChildFragment). It would only fail if you validated the persisted inlined document — which Relay does not do at compile time.
@alias is documented as a client-only compiler/runtime feature (docs). It does not warn that aliased spreads skip merge, so the same compiled _Fragment$defer$Label can be emitted twice in the document the server sees.
Related
Summary
Relay’s
@defertransform namespaceslabelas{owningDefinition}$defer${label}and checks uniqueness while walking each fragment/operation definition once. Persist then inlines fragments. Spreading that same owning fragment on two live paths (typically two@aliasparents) prints the compiled label twice in the persisted operation.That persisted document violates Defer And Stream Directive Labels Are Unique. The compiler still succeeds. The failure shows up only when a GraphQL gateway validates the inlined operation.
This is the same class of bug as #5270 (argument-variant clones keep the original fragment’s compiled label). Here the second copy comes from
@aliasblocking selection merge, not from@argumentsvariants.Reproduction
ParentQuery.Actual
The compiler is green. Persist inlines
ChildFragmentonto both aliased parents, so the operation contains the same compiled label twice, e.g.(or
_ChildFragment$defer$Buttons, depending on compiler version).Without
@alias, Relay merges the twoChildFragmentspreads and persist emits the label once.Expected
One of:
LabelNotUniqueForDeferStreamfor this operation, or@aliaspath or variant name), so persist cannot emit the same identity twice.Why this started 500ing in production
Nothing in the client GraphQL source changed. Federation / Apollo Router began enforcing label uniqueness on the operation the planner expands — the persisted, inlined document Relay already shipped.
Apollo Router v2.10.5 and v2.16.1 release notes: “Update operation validation to enforce unique
@deferlabels.” That was a query-planner DoS fix (duplicate nested@deferlabels could unbounded-recurse; GHSA-gr6h-4wpf-xp52 / AIKIDO-2026-807777). After those patches, a previously accepted persisted query is rejected.Client libraries do not catch this before persist:
defer_stream.rs(record_label+transform_label) runs per definition.ChildFragmentis visited once, soChildFragment$defer$TradeButtonsis recorded once. Persist inlining happens later.specifiedRuleshas noDeferStreamDirectiveLabelRule.Directivenodes in the AST it is given and does not expand spreads. Validating source still passes (one@deferinChildFragment). It would only fail if you validated the persisted inlined document — which Relay does not do at compile time.@aliasis documented as a client-only compiler/runtime feature (docs). It does not warn that aliased spreads skip merge, so the same compiled_Fragment$defer$Labelcan be emitted twice in the document the server sees.Related
@deferlabel uniqueness violation when fragment has@argumentDefinitionsand is spread with different@argumentsin the same query #5270 —@deferlabel uniqueness violation when a fragment with@argumentDefinitionsis spread with different@arguments(variant clones keep the original compiled label). Same spec violation, different Relay trigger.DeferStreamDirectiveLabelRule)