vercel / vercel/next.js

ERR_HTTP_HEADERS_SENT and __next_metadata_boundary__ hydration mismatch when mixing "use cache" with draftMode()/cookies() in same page component

Open
#92,087 1 comment 8 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
JavaScript
Stars
142k
Forks
32.4k
Avg merge
2d 14h
Merged PRs (30d)
351

Description

Link to the code that reproduces this issue

https://github.com/lucianobfs/nextjs-use-cache-metadata-boundary-repro

To Reproduce
  1. Clone the repo and run:
    pnpm install
    pnpm build
    pnpm start
    
  2. Visit http://localhost:3000/posts/1
  3. The page renders correctly (HTTP 200), but check the server terminal for errors

The pattern that triggers the bug (app/posts/[slug]/page.tsx):

  1. generateMetadata() calls await connection() — required because the underlying library uses crypto.randomBytes() (e.g., Effect-TS, Prisma)
  2. The page component calls a "use cache" function first (cachedPostData)
  3. Then accesses draftMode() and searchParams — dynamic APIs called outside the cache scope, as the docs recommend
Current vs. Expected behavior

Current:
The page renders correctly (HTTP 200), but the server logs contain two recurring errors:

Error A — Metadata boundary mismatch (most frequent):
```
Error: Expected the resume to render

in this slot but instead it rendered <next_metadata_boundary>.
The tree doesn't match so React will fallback to client rendering.
{ digest: '2161942505' }
```

Error B — ERR_HTTP_HEADERS_SENT (intermittent):
```
Error: Cannot set headers after they are sent to the client
at g.setHeader (.next/server/chunks/ssr/[root-of-the-server]__*.js)
{ code: 'ERR_HTTP_HEADERS_SENT', digest: '1390897066' }
```

Expected:
No server-side errors. The cached portion and the dynamic portion should coexist without hydration mismatches or header conflicts.

Impact

Although pages return HTTP 200, these errors cause:

  1. Performance degradation: React falls back to full client rendering, discarding streamed SSR output
  2. Wasted server resources: Full SSR work gets thrown away
  3. SEO risk: Crawlers may receive incomplete HTML before client JS executes
  4. Log noise: ~20-30 errors/hour in production with moderate traffic
Observed patterns from production (www.konsi.com.br)
Error Frequency Cache status Routes
__next_metadata_boundary__ mismatch ~20/3h STALE and HIT Pages with generateMetadata + connection() + "use cache" data fn
ERR_HTTP_HEADERS_SENT ~2-12/hr (varies) HIT and PRERENDER Pages calling draftMode() or cookies() after "use cache" fn

Key observations:

  • Error A happens on both cache: STALE (during revalidation) and cache: HIT (serving from cache)
  • Error B happens when the page accesses cookies()/draftMode() after a "use cache" call, on cached or prerendered responses
  • The connection() call in generateMetadata is required because the underlying data library (Effect-TS) uses crypto.randomBytes() internally, which is disallowed during prerendering
  • Both errors come from source: serverless-middleware or source: serverless — framework-level, not application code
Analysis

The root cause appears to be a conflict in how the streaming/resumption protocol handles the transition from a "use cache" boundary to dynamic request APIs within the same server component:

  1. generateMetadata runs with connection() (making it dynamic) and produces metadata
  2. The page component calls the "use cache" function, which fills/serves from cache and begins streaming
  3. After the cache boundary, the page calls draftMode() or cookies(), which attempts to read/set headers
  4. The metadata boundary marker (<__next_metadata_boundary__>) gets injected at a position that doesn't match what React expects during hydration replay — the cache boundary and metadata boundary interleave in a way that changes the DOM structure

The ERR_HTTP_HEADERS_SENT variant occurs when draftMode()/cookies() tries to set response headers after the cached response has already started streaming.

Provide environment information

```
Operating System:
Platform: linux (Vercel serverless) / macOS (local)
Arch: x64
Binaries:
Node: 24.x
pnpm: 9.15.9
Relevant Packages:
next: 16.2.1
react: 19.1.2
react-dom: 19.1.2
```

Which area(s) are affected?
  • App Router: Components, Layouts, Pages
  • App Router: Data Fetching (use cache, cacheLife, cacheTag)
  • App Router: Metadata (generateMetadata)
Additional context

Workarounds attempted:

  • Moving draftMode() before the "use cache" call — breaks prerendering because draftMode() uses crypto internally
  • Wrapping the dynamic section in <Suspense> — does not prevent the header conflict
  • Removing connection() from generateMetadata — causes prerender errors due to library internals using crypto.randomBytes()

The recommended pattern from the docs (call "use cache" functions, then access dynamic APIs) is exactly what triggers this bug. The use cache docs state:

Cached functions and components cannot directly access runtime APIs like cookies(), headers(), or searchParams. Instead, read these values outside the cached scope and pass them as arguments.

We follow this guidance — dynamic APIs are called outside the "use cache" function — but the combination still produces errors when both exist in the same page component.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Clone the linked reproduction, inspect app/posts/[slug]/page.tsx, and run pnpm install, pnpm build, and pnpm start. Visit /posts/1 and inspect the server terminal; done means the page still renders and the metadata-boundary and ERR_HTTP_HEADERS_SENT errors no longer appear.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, nextjs, node.js, react
Domain
backend, frontend, full-stack, performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.