Skip to content

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

Description

@jaysbase

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
}
  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 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions