facebook / facebook/relay

Duplicate compiled @defer labels when the same deferred fragment is inlined twice under @alias

Open
#5,408 1 comment 1 reaction 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.