Duplicate compiled @defer labels when the same deferred fragment is inlined twice under @alias
- Dominant language
- Rust
- Stars
- 19k
- Forks
- 1.9k
- PR merge metrics
- No merged PRs in 30d
Description
## 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](https://spec.graphql.org/draft/#sec-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
```graphql
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
}
```
1. Compile with persist enabled.
2. 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](https://github.com/apollographql/router/releases/tag/v2.10.5) and [v2.16.1](https://github.com/apollographql/router/releases/tag/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](https://github.com/advisories) / [AIKIDO-2026-807777](https://intel.aikido.dev/cve/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`](https://github.com/facebook/relay/blob/main/compiler/crates/relay-transforms/src/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](https://relay.dev/docs/guides/alias-directive/)). 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
- #5270 — `@defer` label uniqueness violation when a fragment with `@argumentDefinitions` is spread with different `@arguments` (variant clones keep the original compiled label). Same spec violation, different Relay trigger.
- [Defer-stream WG discussion on label uniqueness](https://github.com/graphql/defer-stream-wg/discussions/35)
- [graphql-js incremental delivery docs](https://www.graphql-js.org/docs/defer-stream/) (`DeferStreamDirectiveLabelRule`)
Contributor guide
Research direction
Start in compiler/crates/relay-transforms/src/defer_stream.rs, reading record_label and transform_label, then trace how persist inlines fragments. Run the ParentQuery reproduction with persist enabled and inspect the inlined operation. Done means the compiler rejects or re-uniques duplicate @defer labels created by aliased inline sites, with coverage for this reproduction.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- backend-api-design, compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100