Skip to content

PPR metadata resume mismatch despite htmlLimitedBots fully disabling streaming metadata #93401

@mdotk

Description

@mdotk

Link to the code that reproduces this issue

https://github.com/mdotk/next-ppr-metadata-repro

To Reproduce

Clone the repo and run:

npm install
npm run build
npm run start

In another terminal:

npm run repro:check

Or manually request the PPR route with different user agents:

curl -sS \
  -A 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_7) AppleWebKit/537.36 Chrome/124 Safari/537.36' \
  -o /tmp/browser.html \
  http://127.0.0.1:3099/posts/alpha

curl -sS \
  -A 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Googlebot/2.1; +http://www.google.com/bot.html) Safari/537.36' \
  -o /tmp/googlebot.html \
  http://127.0.0.1:3099/posts/alpha

The app uses:

const nextConfig = {
  cacheComponents: true,
  htmlLimitedBots: /.*/,
}

The route is a PPR route in the production build:

└ ◐ /posts/[slug]
  ├ /posts/[slug]
  └ /posts/alpha

Current vs. Expected behavior

Current behavior:

  • htmlLimitedBots: /.*/ should fully disable streaming metadata per the docs.
  • Browser-like UA returns 200, but the initial head does not contain the route metadata and the response contains the hidden streaming metadata wrapper.
  • Googlebot/Bingbot return 200 with route metadata in the initial head.
  • Empty/no-UA requests also stream metadata outside the initial head.
  • The server logs a React resume mismatch:
Error: Expected the resume to render <div> in this slot but instead it rendered <__next_metadata_boundary__>. The tree doesn't match so React will fallback to client rendering.
digest: '2491006542'

Expected behavior:

With htmlLimitedBots: /.*/, the prerender/no-UA path and runtime paths should use a consistent metadata component tree shape, or otherwise avoid a PPR resume mismatch. A documented "fully disable streaming metadata" config should not still allow the PPR shell to be generated with a streaming metadata wrapper.

What appears to be happening

In Next 16.2.3, shouldServeStreamingMetadata(userAgent, ".*") returns false for a non-empty browser UA, Googlebot, and Bingbot. But the app-page template still has a no-UA branch that forces streaming metadata:

const serveStreamingMetadata =
  botType && isRoutePPREnabled
    ? false
    : !userAgent
      ? true
      : shouldServeStreamingMetadata(userAgent, nextConfig.htmlLimitedBots)

That means the PPR prerender/export shell can be built with the streaming metadata tree shape, while runtime requests with non-empty UAs can render the blocking metadata tree shape. The metadata component tree changes from a hidden <div> wrapper to direct <__next_metadata_boundary__>, which matches the resume mismatch.

This is in the same problem area as:

#92087

And this closed PR appears directly relevant:

#90259

Provide environment information

Next.js: 16.2.3
React: 19.1.2
React DOM: 19.1.2
Node.js tested locally: 23.3.0
Runtime: next build + next start
App Router: yes
Cache Components: enabled
PPR: enabled through cacheComponents
htmlLimitedBots: /.*/

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

App Router, Metadata, Partial Prerendering, Runtime

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

next build, next start

Additional context

The minimal reproduction digest is 2491006542. The important signal is the metadata tree-shape mismatch: expected <div>, got <__next_metadata_boundary__>.

Metadata

Metadata

Assignees

No one assigned

    Labels

    MetadataRelated to Next.js' Metadata API.RuntimeRelated to Node.js or Edge Runtime with Next.js.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions