GraphQL Incremental Hydration Preflight
Overview
Use the raw HTTP response as the source of truth. The helper validates HTTP framing, multipart boundaries, strict JSON, a pinned envelope profile, patch targets, and the fully hydrated document. It performs no network access.
The key distinction is protocol profile:
- current ID-based: initial
pending[{id,path}]; patches useidplus optionalsubPath; completion usescompleted[{id}]; - legacy path-based (
legacy-path-v0.1): patches carry absolutepath, may be folded into the initial part, and have nopending/completedIDs.
Mixing these profiles commonly makes a stream patch look valid while a legacy hydrator ignores its items or cannot resolve its location. Never auto-detect or coerce between profiles during incident replay.
When to Use
- A nested
@streamlist or deferred object exists on the wire but is absent after hydration. - A proxy, cache, SSR layer, or GraphQL client upgrade changed incremental response handling.
- The capture contains
pendingandcompletedIDs. - You need a fail-closed rollout gate and an explicit recovery path.
Do not use this helper for live traffic replay, mutation execution, schema correctness, load testing, or an envelope that does not exactly match one of the two named profiles. Redact credentials, cookies, tokens, PII, and unrelated extensions before saving a fixture.
Quick Reference
PYTHONDONTWRITEBYTECODE=1 python3 scripts/preflight.py \
--profile current-id-v1 \
tests/fixtures/response.http \
tests/fixtures/expected.json
PYTHONDONTWRITEBYTECODE=1 python3 -m unittest discover -s tests -v
Exit 0 means the pinned offline replay exactly matches the expected final data. Exit 1 means rollout is blocked. Argument errors exit 2.
Procedure
1. Freeze and redact the failing boundary
Capture the HTTP status line, response headers, and exact multipart bytes at the boundary immediately before the failing hydrator. Record producer, proxy, client/hydrator, and application versions separately. Preserve CRLF and boundary bytes; do not reconstruct them from parsed JSON.
Remove authorization, cookies, request variables, and sensitive response fields without changing envelope keys, IDs, paths, list shape, part order, or framing. Use an equivalent synthetic capture if redaction would alter those properties.
Complete when: one redacted byte capture names both capture boundary and component versions, and no secret or customer data remains.
2. Pin the profile before parsing
For current-id-v1, require:
- initial object-valued
data, non-emptypending, andhasNext: true; - unique non-empty pending IDs with paths that resolve in initial data;
- each incremental entry to reference a live ID and contain exactly one of object-valued
dataor array-valueditems; - optional
subPathto resolve relative to the pending path; - each completion to reference a live ID exactly once;
- no live IDs when terminal
hasNext: falsearrives; - only allowlisted envelope keys. Standard non-semantic metadata belongs under
extensions; an unknown top-level key is not silently ignored.
Pass --profile current-id-v1 explicitly. A missing, misspelled, legacy, or future profile blocks replay rather than triggering format detection.
For a path-based V0.1 capture, pass --profile legacy-path-v0.1; this profile accepts incremental on the initial part and proves those folded patches are merged. It rejects ID lifecycle fields. Do not choose a profile by trial and error: identify the producer contract first.
Complete when: the selected profile is explicit in both command and report, and any profile mismatch exits nonzero without hydrated output.
3. Replay the ID lifecycle and hydration
The replay records each pending ID and its base path. A defer patch merges object fields at base path + subPath. A stream patch appends items to the list at base path + subPath. It then consumes completed IDs and verifies terminal closure.
Compare the result against a separately authored expected final data object. Do not derive expected JSON by running the same hydrator under investigation. A successful parse or hasNext: false alone does not prove hydration.
Complete when: patch count, pending IDs, completed IDs, final canonical digest, and exact expected-document match are recorded.
4. Localize the disappearance
Compare four artifacts in order:
- raw multipart part JSON;
- validated replay output;
- hydrator/cache write immediately after each part;
- post-hydration render or cache read.
If the offline replay blocks, treat the wire contract as invalid or unsupported. If replay passes but the application loses the list, the defect is downstream: inspect profile selection, ID lookup, relative subPath, items append semantics, normalization keys, cache writes, and SSR serialization. Do not “fix” a downstream loss by rewriting upstream payloads until this boundary comparison proves the rewrite is needed.
Complete when: the first boundary where expected data differs is identified with producer and consumer versions.
5. Make the rollout decision
Roll out only when all are true:
- the exact canary capture passes the pinned profile;
- the fully hydrated output exactly matches independently authored expected JSON;
- every pending ID completes once and no patch references an unknown/completed ID;
- unknown envelope keys are absent or deliberately standardized under reviewed
extensionshandling; - the same captured bytes hydrate correctly through the canary client/proxy/SSR path;
- rollback artifacts and owners are ready.
Otherwise the decision is BLOCK. Never strip an unknown key and continue, auto-detect legacy versus current format, drop an items patch, or treat a terminal envelope as success while data is missing.
6. Roll out and recover safely
Canary one producer/proxy/client compatibility tuple at a time. Bound traffic and duration; compare nested-list cardinality and final-document digest with the control. Expand only with zero missing-patch, orphan-ID, duplicate-completion, or hydration-mismatch signals.
On any mismatch:
- stop expansion and route affected traffic to the last known-good tuple;
- disable incremental delivery for the affected operation only if the non-incremental path is pre-tested and semantically equivalent;
- invalidate only cache/SSR artifacts proven to contain partial hydrated data;
- replay retained redacted captures against old and candidate tuples;
- reconcile user-visible partial results from the authoritative backend, not from the damaged cache;
- resume with a new canary only after the pinned replay and end-to-end hydration both pass.
Do not globally disable GraphQL validation, discard unknown keys, flush all caches without scope, or retry non-idempotent operations merely because a response hydration failed.
Fail-Closed Conditions
Block on malformed HTTP/chunking/multipart framing; duplicate JSON keys; BOM, invalid UTF-8, NaN, or infinity; unknown envelope or entry keys; GraphQL errors; profile mismatch; unresolved paths; duplicate/reused/unknown IDs; data/items ambiguity; non-list stream targets; patch conflicts; premature completion; live IDs at terminal closure; absent patches; or final-document mismatch.
extensions is retained as an allowed metadata container but is not interpreted. If extension semantics can change hydration, define a new reviewed replay profile rather than silently accepting them.
Objective Verification
- the fixture hydrates a nested streamed list and deferred object under
current-id-v1; - completion order may differ from pending order but every ID closes exactly once;
- an unknown top-level envelope key exits
1and emits no merged document; - omitted or wrong
--profilecannot replay; - a folded initial path-based payload passes only under
legacy-path-v0.1, while cross-profile fields block rather than being coerced; - malformed framing, duplicate keys, non-finite numbers, GraphQL errors, unresolved/conflicting patches, and expected mismatch block;
- chunked and identity-framed captures produce the same hydrated result;
- tests and helper perform no network access.
Evaluation Prompts
- Normal: “We are rolling out GraphQL
@deferthrough a proxy. The first multipart part may contain bothdataandincremental. Give an executable offline preflight that validates raw HTTP and proves the final merged data.” - Difficult edge: “A nested list patch disappears after hydration. The capture uses
pending/completedIDs and an unknown extra envelope key. Give a fail-closed, profile-pinned replay and rollout/recovery decision.” - Should not activate: “My ordinary GraphQL JSON response has no
@defer,@stream,multipart/mixed, or incremental payloads. Should I use this workflow?” Expected routing: no; use ordinary GraphQL response validation.
Sources and Scope
The current ID-based shape follows GraphQL.js incremental execution interfaces and examples: pending results carry id and absolute path; incremental defer/stream results carry id, optional relative subPath, and data or items; subsequent envelopes may carry completed. GraphQL.js separately documents its legacy path-based format. Incremental delivery remains implementation-sensitive, so this helper intentionally names local replay profiles rather than claiming universal protocol negotiation.
- GraphQL.js current incremental result types (accessed 2026-08-12): https://github.com/graphql/graphql-js/blob/17.x.x/src/execution/incremental/IncrementalExecutor.ts
- GraphQL.js legacy/current comparison (accessed 2026-08-12): https://github.com/graphql/graphql-js/blob/17.x.x/src/execution/legacyIncremental/legacyExecuteIncrementally.ts
- GraphQL-over-HTTP incremental delivery RFC (accessed 2026-08-12): https://github.com/graphql/graphql-over-http/blob/main/rfcs/IncrementalDelivery.md
- Apollo Kotlin folded-initial field-loss report (accessed 2026-08-12): https://github.com/apollographql/apollo-kotlin/issues/6979
- GraphQL Yoga duplicate transfer-encoding report (accessed 2026-08-12): https://github.com/graphql-hive/graphql-yoga/issues/4412
- Relay nested-array hydration loss report (accessed 2026-08-12): https://github.com/facebook/relay/issues/5354
- GraphQL response specification (accessed 2026-08-12): https://spec.graphql.org/draft/#sec-Response
The strict allowlist, profile pin, offline replay, expected-document gate, rollout criteria, and recovery order are original operational recommendations, not claims from the GraphQL specification. No source prose, issue reproducer, or implementation code is copied into the helper or fixtures.