Skip to content

RelayResponseNormalizer: don't write false __isInterface under deferDeduplicatedFields - #5390

Open
jonreading81 wants to merge 1 commit into
facebook:mainfrom
jonreading81:jr-deferdedup-typeregistry-fix
Open

RelayResponseNormalizer: don't write false __isInterface under deferDeduplicatedFields#5390
jonreading81 wants to merge 1 commit into
facebook:mainfrom
jonreading81:jr-deferdedup-typeregistry-fix

Conversation

@jonreading81

Copy link
Copy Markdown

Fixes #5389.

Summary

RelayResponseNormalizer._normalizeTypeDiscriminator and ._normalizeInlineFragment write client:__type:<ConcreteType>.__is<Interface> unconditionally, using Object.prototype.hasOwnProperty.call(data, abstractKey) as the value. Under Environment({ deferDeduplicatedFields: true }), that misinterprets a legitimately-omitted abstract typename in an incremental @defer payload as "type does not implement" — it writes false into the type registry, DataChecker reads it and takes the "skip fragment" branch, and every subsequent ... on <Interface> selection on that concrete type is silently dropped without a missing-data signal. Downstream useLazyLoadQuery / usePreloadedQuery report available, never fetch, and reads inside the fragment collapse to null under $isWithinUnmatchedTypeRefinement.

Full reproduction and evidence-based trace in #5389.

Change

Only skip the type-registry write when implementsInterface === false && this._deferDeduplicatedFields. When the write is skipped, DataChecker treats the registry entry as unknown (_implementsInterface == null) → _handleMissing() → fetches on next check → response arrives with the tag → true is written normally.

Environments that don't opt into deferDeduplicatedFields are byte-identical to before: the missing tag is still an authoritative "type doesn't implement" signal there, and false is still written.

Both write sites (TypeDiscriminator in _traverseSelections and the abstractKey != null branch of _normalizeInlineFragment) get the same guard.

Test plan

  • Manual repro on our app (branded @defer + deferDeduplicatedFields) — the corrupted __type:X.__is<Interface>: false writes are gone, downstream queries fetch correctly. Confirmed by instrumenting store.publish and dumping the registry.
  • Running the existing relay-runtime test suite locally against this branch — happy to add a targeted test if you'd like one for the deferDeduplicatedFields + missing tag interaction.

Notes

Filed as a targeted, minimal patch — the wider question of whether writing false on a missing abstract typename is ever the right thing (even without deferDeduplicatedFields) can be answered separately. The narrower guard here keeps the blast radius as small as possible.

@meta-cla meta-cla Bot added the CLA Signed label Aug 4, 2026
@jonreading81
jonreading81 force-pushed the jr-deferdedup-typeregistry-fix branch 2 times, most recently from 98b3652 to 5e910cd Compare August 4, 2026 13:00
…eduplicatedFields

RelayResponseNormalizer's TypeDiscriminator and InlineFragment
normalization paths unconditionally write
'client:__type:<ConcreteType>.__is<Interface>: <hasOwnProperty>'
during payload traversal.

Without deferDeduplicatedFields, this is fine: a missing __isFoo tag
in a normalized payload is a real 'type does not implement Foo'
signal, and the false write is load-bearing so DataChecker can skip
future '... on Foo' selections for that concrete type.

Under Environment({ deferDeduplicatedFields: true }), the same missing
tag can just mean 'delivered earlier in a different chunk of this
operation, dedup'd out of the current chunk'. Writing false in that
case is wrong: it plants a schema-contradicting negative claim in the
type registry, and DataChecker (which treats false as authoritative
non-implementation) then silently skips '... on <Interface>' subtrees
for every future query touching that concrete type — never marking
anything missing, never fetching.

Symptom in our app: after an SSR page load that fetches a defer'd
query, client-navigations to any page that reads a Cloudcast via
'node(id) { ... on Node { ... on Cloudcast ... } }' rendered null for
every field inside — reader saw $isWithinUnmatchedTypeRefinement:true
because DataChecker had marked the query 'available' without ever
fetching, thanks to __type:Cloudcast.__isNode=false written during the
defer'd chunk.

Fix: skip the write when 'hasOwnProperty === false && this
._deferDeduplicatedFields'. When the write is skipped, DataChecker
treats the registry entry as unknown (_implementsInterface == null) →
handles missing → fetches on next check → the response arrives with
the tag → true written normally. Non-dedup environments are
byte-identical to the previous behaviour.

Filed as facebook#5389.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RelayResponseNormalizer writes __is<Interface>: false when @defer'd payload omits abstract typename, corrupting DataChecker

1 participant