Skip to content

PPR resume reads the request body as UTF-8 without decompressing Content-Encoding: gzip → "Invariant: invalid postponed state" #95214

Description

@arjenblokzijl

Link to the code that reproduces this issue

https://github.com/arjenblokzijl/next-ppr-gzip-resume-repro

To Reproduce

  1. npm install

  2. npm run repro:unit — deterministic, server-free proof. It calls the installed
    parsePostponedState exactly as base-server.js does on a resume
    (body.toString("utf8")), passing a gzip-compressed postponed state. Output:

    gzip header bytes: 1f 8b 08 00 (matches what we see in production)
    Failed to parse postponed state Error: Invariant: invalid postponed state
    -> parsePostponedState degraded to type:1 (logged error, no crash; HTTP 200)

  3. npm run build — the single route reports as ◐ (Partial Prerender).

(The end-to-end path that calls the parser is driven by the platform's
internal resume routing — minimal mode + the prerender manifest — so a bare
next start doesn't exercise it; the unit repro isolates the failure
deterministically. See the repo README for the full code-path trace.)

Current vs. Expected behavior

On a PPR (Cache Components) resume, the postponed state is read from the POST
request body and decoded with body.toString("utf8") without honoring
Content-Encoding
. When the resume body arrives gzip-compressed (as it
does behind Vercel's infrastructure), the gzip bytes decoded as UTF-8 are not a
valid postponed-state string, so parsePostponedState fails its ^<digits>:
check, logs Failed to parse postponed state + the Invariant, and degrades to
type:1
— a logged server error with an HTTP 200 fallback (the route cannot
resume its prerendered HTML). In production this fires steadily on every resumed
route (cart/checkout/category pages, etc.).

Expected: the resume body is decompressed according to its Content-Encoding
(gzip/br/deflate) before toString("utf8")/parsePostponedState.

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 25.5.0: Mon Apr 27 20:41:06 PDT 2026; root:xnu-12377.121.6~2/RELEASE_ARM64_T6030
  Available memory (MB): 18432
  Available CPU cores: 12
Binaries:
  Node: 24.17.0
  npm: 11.13.0
  Yarn: N/A
  pnpm: 9.5.0
Relevant Packages:
  next: 16.3.0-preview.5 // Latest available version is detected (16.3.0-preview.5).
  eslint-config-next: N/A
  react: 19.2.4
  react-dom: 19.2.4
  typescript: 5.9.3
Next.js Config:
  output: N/A

Which area(s) are affected? (Select all that apply)

cacheComponents

Which stage(s) are affected? (Select all that apply)

Vercel (Deployed)

Additional context

Root cause (code references)

  • server/base-server.js — resume-body read:
  if (this.isAppPPREnabled && this.minimalMode &&
   req.headers[NEXT_RESUME_HEADER] === '1' && req.method === 'POST') {
 const body = await readBodyWithSizeLimit(req.body, maxPostponedStateSizeBytes);
 const postponed = body.toString('utf8');   // ← no Content-Encoding decompression
 addRequestMeta(req, 'postponed', postponed);
  }
  • server/lib/postponed-request-body.js — readBodyWithSizeLimit just concatenates
    raw chunks; no decompression.
  • server/app-render/postponed-state.js — parsePostponedState requires the state
    to start with ^([0-9]*):, throws Invariant: invalid postponed state (E314),
    and its own catch logs Failed to parse postponed state and returns a degraded
    type:1.

The garbled state in the error starts with the gzip magic header 1f 8b 08.

  • Not a regression — the read path (body.toString('utf8'), no decompression)
    is byte-for-byte identical in 16.2.x and 16.3, so it reproduces on both. The
    repro pins 16.3.0-preview.5 for convenience.
  • Workaround: make affected routes fully dynamic (ƒ) so there's no postponed
    state to resume (e.g. await connection() at the page root — 16.3 only). This
    trades away the static shell, so it's only viable for session-specific routes,
    not cacheable catalog/content pages.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions