Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
f2e4c4f347 Fix hang on repeated dynamic imports of modules with parse errors
When a module with parse errors is imported twice using dynamic import(),
the second import would hang indefinitely. The root cause was that JSC's
module loader caches the module entry in esmRegistryMap after the first
import attempt. On the second import, JSC skips calling moduleLoaderFetch
because it sees the entry already exists in the registry, and waits for
a cached promise that was never properly resolved.

The fix removes failed modules from the esmRegistryMap when the fetch
promise is rejected (e.g., due to parse errors). This allows subsequent
import attempts to retry the module loading from scratch.

Fixes #23139

Test: test/regression/issue/23139.test.ts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-13 00:03:26 +00:00
2 changed files with 65 additions and 3 deletions

View File

@@ -3329,13 +3329,24 @@ JSC::JSInternalPromise* GlobalObject::moduleLoaderFetch(JSGlobalObject* globalOb
RETURN_IF_EXCEPTION(scope, rejectedInternalPromise(globalObject, scope.exception()->value()));
ASSERT(result);
JSC::JSInternalPromise* promiseToReturn = nullptr;
if (auto* internalPromise = JSC::jsDynamicCast<JSC::JSInternalPromise*>(result)) {
return internalPromise;
promiseToReturn = internalPromise;
} else if (auto* promise = JSC::jsDynamicCast<JSC::JSPromise*>(result)) {
return jsCast<JSC::JSInternalPromise*>(promise);
promiseToReturn = jsCast<JSC::JSInternalPromise*>(promise);
} else {
return rejectedInternalPromise(globalObject, result);
promiseToReturn = rejectedInternalPromise(globalObject, result);
}
// If the promise is rejected (e.g. parse error), remove from registry
// to allow retry on subsequent imports (issue #23139)
if (promiseToReturn && promiseToReturn->status(vm) == JSC::JSPromise::Status::Rejected) {
auto* zigGlobalObject = static_cast<Zig::GlobalObject*>(globalObject);
auto* map = zigGlobalObject->esmRegistryMap();
map->remove(globalObject, key);
}
return promiseToReturn;
}
JSC::JSObject* GlobalObject::moduleLoaderCreateImportMetaProperties(JSGlobalObject* globalObject,

View File

@@ -0,0 +1,51 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
test("repeated dynamic imports of file with parse error should not hang (#23139)", async () => {
using dir = tempDir("issue-23139", {
"repro.js": /* js */ `
try {
console.log("begin import 1");
await import("./invalid_code");
} catch(e) {
console.log("error 1");
}
try {
console.log("begin import 2");
await import("./invalid_code");
} catch(e) {
console.log("error 2");
}
`,
"invalid_code": /* js */ `
def hello():
print("Hello from Python!")
return "This is Python code"
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "repro.js"],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const timeout = setTimeout(() => {
proc.kill();
}, 5000);
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
clearTimeout(timeout);
// Should not hang - both imports should complete
expect(stdout).toContain("begin import 1");
expect(stdout).toContain("error 1");
expect(stdout).toContain("begin import 2");
expect(stdout).toContain("error 2");
// Should exit cleanly
expect(exitCode).toBe(0);
});