vercel / vercel/next.js

turbopackModuleFragments: split_module mixes group and module indices, panics at module_fragments/graph.rs:746 on a bare app

Open
#98,128 1 comment 0 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/benderTheCrime/turbopack-module-fragments-repro

To Reproduce

Pages Router, five files, no dependencies beyond next + react + react-dom:

mkdir bare && cd bare && mkdir pages
npm install next@16.3.4 react@19.2.8 react-dom@19.2.8

package.json

{ "name": "bare", "private": true, "scripts": { "build": "next build --turbopack" } }

next.config.js

module.exports = { experimental: { turbopackModuleFragments: true } };

pages/index.js

export default function Home() { return <div>hello</div>; }
npx next build --turbopack
Current vs. Expected Behavior

Expected: the build succeeds, or the flag degrades gracefully.

Actual: the build panics and never produces a manifest.

thread 'tokio-rt-worker' panicked at
turbopack/crates/turbopack-ecmascript/src/module_fragments/graph.rs:746:16:
index out of bounds: the len is 8 but the index is 9
- Execution of EcmascriptModulePartAsset::select_part failed
- Execution of split_module failed
- index out of bounds: the len is 8 but the index is 9

Setting turbopackModuleFragments: false on the identical tree builds cleanly (exit 0).

The module that panics is Next's own next/dist/esm/server/pipe-readable.js; it surfaces as
render-result.js: Module not found: Can't resolve './pipe-readable'. A second, independent
symptom of the same defect appears on next/dist/esm/server/response-cache/utils.js:

Export fromResponseCacheEntry doesn't exist in target module
The export fromResponseCacheEntry was not found in module
  .../server/response-cache/utils.js [ssr] (ecmascript) <internal part 9>.
Did you mean to import routeKindToIncrementalCacheKind?

— i.e. the off-by-N lands on the neighbouring fragment. No user code is involved in either.

Root cause

split_module mixes two index spaces:

  • group indexfor (ix, group) in groups.graph_ix.iter().enumerate() (L359)
  • modules position — where a chunk actually lands in modules

Empty groups are skipped and never pushed:

if chunk.body.is_empty() { continue; }   // L716
modules.push(chunk);                     // L720

but the module-evaluation index and the dependency map are recorded in group space:

module_evaluation_ix = Some(ix as u32);            // L455
outputs.insert(Key::ModuleEvaluation, ix as u32);  // L456
part_deps.entry(ix as u32)                         // L402-403, L490-491

while outputs is otherwise written in modules-position space:

outputs.insert(Key::Exports, modules.len() as u32);          // L723
outputs.insert(Key::ModuleEvaluation, modules.len() as u32); // L736 (is_none branch)

and then read as a modules position:

modules[module_evaluation_ix.unwrap() as usize]  // L746  <-- panic

Every skipped empty group makes the group index overshoot. At L746
len == non_empty_groups + 1 (the exports_module pushed at L731), so a panic needs
≥ 2 empty groups. An overshoot of exactly 1 does not panic — it silently writes
export {} into the wrong fragment and leaves outputs[Key::ModuleEvaluation] pointing at
the wrong one, so a build that happens not to crash is not necessarily correct.

Note: #97811 attributed this to module_evaluation_ix being "recorded as modules.len()
and modules later holding fewer entries". That isn't it — modules never shrinks. L455
records the group index, which is a different quantity from modules.len() whenever any
earlier group was empty.

Evidence that it is the index space, not a module shape

Prepending a single top-level side-effectful statement to pipe-readable.js — enough that its
module-evaluation group is no longer empty and cannot be skipped at L716 — removes the panic
entirely
. Patching response-cache/utils.js the same way keeps the panic gone but exposes the
rest of the defect:

part_id is out of range: 10 >= 8; asset = .../server/pipe-readable.js
entrypoints = {ModuleEvaluation: 0, Export("pipeToNodeResponse"): 0,
               Export("pipeNodeReadableToNodeResponse"): 4,
               Export("isAbortError"): 10, Exports: 7}
part_deps   = {0: [Internal(9), Internal(1), Internal(4), ...], 9: [...], 10: [...], ...}

Eight fragments exist (0–7), yet Export("isAbortError") points at part 10 and part_deps is
keyed on 9 and 10. So entrypoints and part_deps are both emitting group indices into a
compacted vector; module_evaluation_ix is just the one that panics first.

Iteratively applying that patch to each module the build names does not converge: after two
patches the next failure is part_id is out of range: 10 >= 8 on an already-patched module.
There is no module-level workaround, which is the practical reason this flag is unusable rather
than merely fragile.

Suggested fix

Keep one mapping from group index to modules position, built as chunks are pushed, and translate
module_evaluation_ix, outputs/entrypoints, and part_deps through it before returning
SplitModuleResult — or stop compacting and push empty chunks so the two spaces stay aligned.

(Not validated against a from-source build — I read the released source rather than patching the
compiler.)

Verified on
version result
16.3.0 panics, len 8 / index 9
16.3.4 (latest stable) panics, identical
16.4.0-canary.13 panics, identical

graph.rs L455, L456, L716, L720 and L746 are byte-identical across all three, so this is
untouched on canary. Reproduced on macOS 15 arm64, Node 24, bun — complementing #97811's
Linux x86_64 / Docker / npm+pnpm and App Router report, so it is neither platform- nor
router-specific.

Related
  • #97811 — same panic site, auto-closed for a missing reproduction link; root cause there is
    misattributed (see note above).
  • #86571 — tree_shake/graph.rs:743, index out of bounds: the len is 80 but the index is 80
    under turbopackTreeShaking. Same shape of defect in the sibling code path; likely the same
    underlying compaction bug and worth fixing together.

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

Reproduce the failure with the five-file Pages Router app and inspect turbopack/crates/turbopack-ecmascript/src/module_fragments/graph.rs at lines 455, 456, 716, 720, and 746. Trace how group indices become positions in the compacted modules vector, then verify the fix against the reported panic, wrong-fragment symptoms, and the turbopackModuleFragments build completing successfully.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, nextjs, rust
Domain
build-system, compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.