turbopackModuleFragments: split_module mixes group and module indices, panics at module_fragments/graph.rs:746 on a bare app
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 index —
for (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_ixbeing "recorded asmodules.len()
andmoduleslater holding fewer entries". That isn't it —modulesnever shrinks. L455
records the group index, which is a different quantity frommodules.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
underturbopackTreeShaking. Same shape of defect in the sibling code path; likely the same
underlying compaction bug and worth fixing together.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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