mirror of
https://github.com/oven-sh/bun
synced 2026-02-23 01:01:47 +00:00
fix: prevent double-fulfill of fetch promise when import() and require() race on the same ESM module
When `import()` starts fetching an ESM module, JSC's `requestFetch` creates
`entry.fetch` as a `.then()` chained promise with a pending microtask reaction.
If `require()` then loads the same module, `fetchCommonJSModuleNonBuiltin`
called `provideFetch` which fulfilled that same promise directly via
`fulfillFetch`. When the pending microtask reaction later fired during
microtask draining, it attempted to resolve the already-fulfilled promise,
triggering `ASSERT(status() == Status::Pending)` in `JSPromise::fulfillPromise`.
This was exposed by commit 21c3439bb4 which added `Loader.registry.$delete(id)`
to the require() error path. The underlying issue is that Bun was calling
`provideFetch` on a module whose fetch pipeline was already owned by import().
The fix has two parts:
1. **ModuleLoader.cpp**: Extend the existing
`hasAlreadyLoadedESMVersionSoWeShouldntTranspileItTwice` guard to also
detect when `import()` has a fetch in progress (entry exists with a fetch
promise but state is still `Fetch`). This prevents
`fetchCommonJSModuleNonBuiltin` from calling `provideFetch` on that entry.
2. **CommonJS.ts**: In `loadEsmIntoCjs`, clear `entry.fetch` before calling
`$fulfillModuleSync`, so that `provideFetch` → `fulfillFetch` creates a
fresh promise instead of fulfilling import()'s pending chained promise.
Closes #12910
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -768,7 +768,17 @@ JSValue fetchCommonJSModule(
|
||||
RETURN_IF_EXCEPTION(scope, false);
|
||||
|
||||
int status = entry.getObject()->getDirect(vm, WebCore::clientData(vm)->builtinNames().statePublicName()).asInt32();
|
||||
return status > JSModuleLoader::Status::Fetch;
|
||||
if (status > JSModuleLoader::Status::Fetch)
|
||||
return true;
|
||||
|
||||
// Also skip when import() has already started fetching this module
|
||||
// (entry exists with a fetch promise). Calling provideFetch in
|
||||
// fetchCommonJSModuleNonBuiltin would fulfill that promise directly,
|
||||
// conflicting with the pending microtask reaction from import()'s
|
||||
// fetch chain and causing a double-fulfill assertion.
|
||||
// https://github.com/oven-sh/bun/issues/12910
|
||||
JSValue fetchValue = entry.getObject()->getDirect(vm, JSC::Identifier::fromString(vm, "fetch"_s));
|
||||
return fetchValue && !fetchValue.isUndefined();
|
||||
}();
|
||||
RETURN_IF_EXCEPTION(scope, {});
|
||||
|
||||
|
||||
@@ -205,6 +205,15 @@ export function loadEsmIntoCjs(resolvedSpecifier: string) {
|
||||
(!$isPromise(fetch) ||
|
||||
($getPromiseInternalField(fetch, $promiseFieldFlags) & $promiseStateMask) === $promiseStatePending))
|
||||
) {
|
||||
// If import() already started fetching this module, entry.fetch is a
|
||||
// chained promise with a pending microtask reaction from the raw fetch
|
||||
// promise. Clear it so $fulfillModuleSync → provideFetch → fulfillFetch
|
||||
// creates a fresh promise instead of fulfilling the old one.
|
||||
// https://github.com/oven-sh/bun/issues/12910
|
||||
if (entry) {
|
||||
entry.fetch = undefined;
|
||||
}
|
||||
|
||||
// force it to be no longer pending
|
||||
$fulfillModuleSync(key);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user