diff --git a/src/bun.js/bindings/ModuleLoader.cpp b/src/bun.js/bindings/ModuleLoader.cpp index a647e3d52a..bc8f3b9ba4 100644 --- a/src/bun.js/bindings/ModuleLoader.cpp +++ b/src/bun.js/bindings/ModuleLoader.cpp @@ -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, {}); diff --git a/src/js/builtins/CommonJS.ts b/src/js/builtins/CommonJS.ts index 466be81507..5990bd183f 100644 --- a/src/js/builtins/CommonJS.ts +++ b/src/js/builtins/CommonJS.ts @@ -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);