Skip to content

[BUG] enableCacheInterception never serves segment prefetches on Next 16, causing an unbounded prefetch loop #1212

Description

@midildar

[BUG] enableCacheInterception never serves segment prefetches on Next 16, causing an unbounded prefetch loop

Summary

On Next 16, the cache interceptor's segment-response branch is unreachable. Every
Next-Router-Segment-Prefetch request is answered with the full-page RSC
payload and full-page headers
, even when the requested segment is present in
the cache entry.

The client router never receives the segment it asked for, never records the
prefetch as satisfied, and re-requests indefinitely. An idle App Router landing
page with four <Link>s in view produced ~780 requests in two minutes and
did not stop.

The trigger is a truthiness check: the branch is gated on
!NextConfig.experimental?.prefetchInlining, which reads as though written when
that setting was a boolean. Next 16 defaults it to an object, so the negation
is always false.

This affects every Next 16 app running enableCacheInterception: true — no
experimental configuration is required to hit it.

Environment

@opennextjs/aws 4.1.0 (latest)
@opennextjs/cloudflare 1.20.2 (latest)
next 16.3.0 (latest)
wrangler 4.114.0
Runtime opennextjs-cloudflare preview (workerd, local)

App shape: App Router, no middleware, ppr: false, clientSegmentCache
unset, no experimental block in next.config.ts at all. Three SSG routes
via generateStaticParams with export const revalidate = 300, plus a static
index that links to them.

// open-next.config.ts
export default defineCloudflareConfig({
  incrementalCache: withRegionalCache(r2IncrementalCache, { mode: 'long-lived' }),
  enableCacheInterception: true,
});

Reproduction

  1. Build and run opennextjs-cloudflare preview --env production.
  2. Open a page containing <Link>s to prerendered routes.
  3. Leave the tab idle and watch the network panel or the wrangler request log.

Evidence

1. A/B on identical code, only the flag differing

enableCacheInterception Requests after load + 30s idle
true hundreds, repeating one _rsc hash, never settles
false 33, then silence

Not a Next-only bug: next start on the same build does not loop. (It does
duplicate prefetches 2–3×, which looks like
vercel/next.js#85489 — the
interceptor turns that bounded duplication into something unbounded.)

2. Segment prefetch is answered with the full page

curl -D - "http://localhost:8787/category/cpu?_rsc=probe" \
  -H "RSC: 1" -H "Next-Router-Prefetch: 1" \
  -H "Next-Router-Segment-Prefetch: /_tree"

enableCacheInterception: true — plain and segment prefetch return
byte-identical bodies (12864 bytes) and identical headers:

content-type: text/x-component
x-nextjs-prerender: 1
x-nextjs-stale-time: 300
x-opennext-cache: HIT

enableCacheInterception: false (NextServer handling it) — the two diverge,
as expected:

plain prefetch    -> x-nextjs-prerender: 1,1   x-nextjs-stale-time: 300
segment prefetch  -> x-nextjs-prerender: 1     x-nextjs-postponed: 2   (no stale-time)

3. The segment is in the cache — it just isn't served

.open-next/cache/<buildId>/category/cpu.cache:

full rsc                                        12857 bytes
segment /_tree                                    959 bytes
segment /_full                                  12857 bytes
segment /category/$d$handle/__PAGE__            10852 bytes
segment /category/$d$handle/@modal/__DEFAULT__    307 bytes

segmentHeader in cachedValue.segmentData is true for /_tree, and a
959-byte payload is available. The interceptor returns the 12857-byte full page
regardless.

Root cause

packages/open-next/src/core/routing/cacheInterceptor.ts, in
getBodyForAppRouter:

const segmentHeader = `${event.headers[NEXT_SEGMENT_PREFETCH_HEADER]}`;
const isSegmentResponse =
  Boolean(segmentHeader) &&
  segmentHeader in (cachedValue.segmentData || {}) &&
  !NextConfig.experimental?.prefetchInlining;   // <-- always false on Next 16

const body = isSegmentResponse ? cachedValue.segmentData[segmentHeader] : cachedValue.rsc;

On Next 16.3.0, with no experimental block configured,
.next/required-server-files.json contains:

"prefetchInlining": { "maxSize": 2048, "maxBundleSize": 10240 }

An object is truthy, so !NextConfig.experimental?.prefetchInlining evaluates to
false and isSegmentResponse can never be true. The segment branch is dead
code on Next 16.

Because additionalHeaders is only populated on that branch, the response also
omits x-nextjs-postponed, so the client can't even tell it received something
other than what it asked for.

Suggested fix

The check appears to predate prefetchInlining becoming an object. Two
directions, depending on the original intent of gating segment responses on
inlining at all:

  1. Test the setting in a shape-aware way rather than by truthiness.
  2. Drop it and rely on the segmentData membership test that already precedes it.

Independently, a safety net would help: when Next-Router-Segment-Prefetch is
present and the segment can't be served as a segment, returning event and
letting NextServer handle it would avoid answering with a different response
shape than the client requested. That matches the interceptor's documented
contract that it "should fallback to the NextServer if the cache is not found".

Minor, adjacent

const segmentHeader = `${event.headers[NEXT_SEGMENT_PREFETCH_HEADER]}`;

A missing header stringifies to the literal "undefined", so
Boolean(segmentHeader) is always true. Harmless today, since
"undefined" in segmentData is false, but the guard doesn't do what it looks
like it does.

Docs note

enableCacheInterception is in DangerousOptions and defaults to false, so
this is opt-in — but the only documented incompatibility is PPR, and nothing
mentions RSC or segment prefetching. Given Next 16's segment prefetching, the
practical cost of enabling it is now considerably worse than "slightly improve
cold start performance". A documentation warning would help even if the code fix
doesn't land.


Happy to put together a minimal reproduction repo if that would help.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions