From 93fe8a74c42a188442b3f71a1b02c69d753ff0af Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Mon, 11 May 2026 22:48:13 -0700 Subject: [PATCH 1/2] =?UTF-8?q?test:=20pin=20#23139=20=E2=80=94=20repeated?= =?UTF-8?q?=20dynamic=20import=20of=20unparseable=20file=20no=20longer=20h?= =?UTF-8?q?angs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reproduces on 1.3.13; fixed on main since the module-loader rewrite window (#29393 → #30262, before #30527). Issue was never re-tested after that landed. Fixes #23139 --- test/regression/issue/23139.test.ts | 49 +++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 test/regression/issue/23139.test.ts diff --git a/test/regression/issue/23139.test.ts b/test/regression/issue/23139.test.ts new file mode 100644 index 000000000000..bdf4c1eed424 --- /dev/null +++ b/test/regression/issue/23139.test.ts @@ -0,0 +1,49 @@ +// https://github.com/oven-sh/bun/issues/23139 +// +// Dynamically importing a file that fails to parse threw on the first +// `await import()`, but the *second* `await import()` of the same path +// hung forever instead of re-throwing — the rejected module-registry +// entry was never re-queried. Fixed somewhere in the WebKit +// module-loader rewrite window (oven-sh/bun#29393 → #30262); reproduces +// on 1.3.13, gone on main with WebKit 88b2f7a2 (i.e. before #30527). +// This test pins it. + +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; + +test("repeated dynamic import of a file that fails to parse re-throws instead of hanging", async () => { + using dir = tempDir("issue-23139", { + "bad.json": `{ "invalid": json }`, + "entry.ts": ` + for (const attempt of [1, 2]) { + try { + await import("./bad.json"); + console.log("attempt " + attempt + ": resolved (unexpected)"); + } catch (e) { + console.log("attempt " + attempt + ": threw " + (e as Error).name); + } + } + console.log("done"); + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "entry.ts"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + // Before the fix the second import never settles, so bound the + // subprocess and assert on signalCode rather than letting the test + // itself time out. + timeout: 10_000, + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stdout).toBe("attempt 1: threw SyntaxError\nattempt 2: threw SyntaxError\ndone\n"); + if (exitCode !== 0) expect(stderr).toBe(""); + // null ⇒ exited on its own; non-null ⇒ killed by the spawn timeout (hung). + expect(proc.signalCode).toBeNull(); + expect(exitCode).toBe(0); +}, 30_000); From 17a74b78fe94bde9ca28f95e3db132f5e6c2e094 Mon Sep 17 00:00:00 2001 From: Alistair Smith Date: Mon, 11 May 2026 22:53:49 -0700 Subject: [PATCH 2/2] =?UTF-8?q?test:=20pin=20#22743=20=E2=80=94=20same=20m?= =?UTF-8?q?odule-registry=20re-entry=20as=20#23139,=20X/Y/X=20interleave?= =?UTF-8?q?=20variant?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/regression/issue/22743.test.ts | 54 +++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 test/regression/issue/22743.test.ts diff --git a/test/regression/issue/22743.test.ts b/test/regression/issue/22743.test.ts new file mode 100644 index 000000000000..5a0a1ea2eadb --- /dev/null +++ b/test/regression/issue/22743.test.ts @@ -0,0 +1,54 @@ +// https://github.com/oven-sh/bun/issues/22743 +// +// Regressed 1.2.20 → 1.2.21: dynamic-importing path X that throws, +// then a *different* error-throwing path Y, then X again — the third +// `await import(X)` hung instead of re-throwing. Same already-settled +// module-registry-entry re-entry as #23139, just with the X/Y/X +// interleave that the original repro hit via two distinct https URLs. +// Fixed in the module-loader rewrite window (#29393 → #30262); this +// test pins the network-free reduction. + +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; + +test("re-importing an error-throwing module after a different error-throwing import re-throws instead of hanging", async () => { + using dir = tempDir("issue-22743", { + "bad1.json": `{ "a": nope }`, + "bad2.json": `{ "b": also nope }`, + "entry.ts": ` + // Original repro used two https:// URLs (which Bun rejects without + // a network call); two unparseable JSON files give the same + // throws-on-load shape without depending on the https-import error + // message staying stable. + const seq = ["./bad1.json", "./bad2.json", "./bad1.json"]; + for (const [i, p] of seq.entries()) { + try { + await import(p); + console.log("import " + (i + 1) + ": resolved (unexpected)"); + } catch (e) { + console.log("import " + (i + 1) + ": threw " + (e as Error).name); + } + } + console.log("done"); + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "entry.ts"], + cwd: String(dir), + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + // Pre-fix the third import never settles; bound the subprocess so the + // assertions report a clear diff instead of the test itself timing out. + timeout: 10_000, + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stdout).toBe("import 1: threw SyntaxError\nimport 2: threw SyntaxError\nimport 3: threw SyntaxError\ndone\n"); + if (exitCode !== 0) expect(stderr).toBe(""); + // null ⇒ exited on its own; non-null ⇒ killed by the spawn timeout (hung). + expect(proc.signalCode).toBeNull(); + expect(exitCode).toBe(0); +}, 30_000);