aboutsummaryrefslogtreecommitdiff
path: root/test/regression/issue
diff options
context:
space:
mode:
Diffstat (limited to 'test/regression/issue')
-rw-r--r--test/regression/issue/02499-repro.ts21
-rw-r--r--test/regression/issue/02499.test.ts95
2 files changed, 116 insertions, 0 deletions
diff --git a/test/regression/issue/02499-repro.ts b/test/regression/issue/02499-repro.ts
new file mode 100644
index 000000000..3c50e53e5
--- /dev/null
+++ b/test/regression/issue/02499-repro.ts
@@ -0,0 +1,21 @@
+const server = Bun.serve({
+ port: 0,
+ async fetch(req) {
+ console.log(await req.json());
+ return new Response();
+ },
+});
+console.log(
+ JSON.stringify({
+ hostname: server.hostname,
+ port: server.port,
+ }),
+);
+
+(async function () {
+ for await (let line of console) {
+ if (line === "--CLOSE--") {
+ process.exit(0);
+ }
+ }
+})();
diff --git a/test/regression/issue/02499.test.ts b/test/regression/issue/02499.test.ts
new file mode 100644
index 000000000..da114d95d
--- /dev/null
+++ b/test/regression/issue/02499.test.ts
@@ -0,0 +1,95 @@
+import { expect, it } from "bun:test";
+import { bunExe, bunEnv } from "../../harness.js";
+import { mkdirSync, rmSync, writeFileSync, readFileSync, mkdtempSync } from "fs";
+import { tmpdir } from "os";
+import { dirname, join } from "path";
+import { sleep, spawn, spawnSync, which } from "bun";
+
+// https://github.com/oven-sh/bun/issues/2499
+it("onAborted() and onWritable are not called after receiving an empty response body due to a promise rejection", async testDone => {
+ var timeout = AbortSignal.timeout(10_000);
+ timeout.onabort = e => {
+ testDone(new Error("Test timed out, which means it failed"));
+ };
+
+ const body = new FormData();
+ body.append("hey", "hi");
+
+ // We want to test that the server isn't keeping the connection open in a
+ // zombie-like state when an error occurs due to an unhandled rejected promise
+ //
+ // At the time of writing, this can only happen when:
+ // - development mode is enabled
+ // - the server didn't send the complete response body in one send()
+ // - renderMissing() is called
+ //
+ // In that case, it finalizes the response in the middle of an incomplete body
+ //
+ // On an M1, this reproduces 1 out of every 4 calls to this function
+ // It's inherently going to be flaky without simulating system calls or overriding libc
+ //
+ // So to make sure we catch it
+ // 1) Run this test 40 times
+ // 2) Set a timeout for this test of 10 seconds.
+ //
+ // In debug builds, this test should complete in 1-2 seconds.
+ for (let i = 0; i < 40; i++) {
+ let bunProcess;
+ try {
+ bunProcess = spawn({
+ cmd: [bunExe(), "run", join(import.meta.dir, "./02499-repro.ts")],
+ stdin: "pipe",
+ stderr: "ignore",
+ stdout: "pipe",
+ env: bunEnv,
+ });
+
+ const reader = bunProcess.stdout?.getReader();
+ let hostname, port;
+ {
+ const chunks = [];
+ var decoder = new TextDecoder();
+ while (!hostname && !port) {
+ // @ts-expect-error TODO
+ var { value, done } = await reader?.read();
+ if (done) break;
+ if (chunks.length > 0) {
+ chunks.push(value);
+ }
+ try {
+ if (chunks.length > 0) {
+ value = Buffer.concat(chunks);
+ }
+
+ ({ hostname, port } = JSON.parse(decoder.decode(value).trim()));
+ } catch {
+ chunks.push(value);
+ }
+ }
+ }
+
+ try {
+ await fetch(`http://${hostname}:${port}/upload`, {
+ body,
+ keepalive: false,
+ method: "POST",
+ timeout: true,
+ signal: timeout,
+ });
+ } catch (e) {}
+
+ bunProcess.stdin?.write("--CLOSE--");
+ await bunProcess.stdin?.flush();
+ await bunProcess.stdin?.end();
+ expect(await bunProcess.exited).toBe(0);
+ } catch (e) {
+ timeout.onabort = () => {};
+ testDone(e);
+ throw e;
+ } finally {
+ bunProcess?.kill(9);
+ }
+ }
+ timeout.onabort = () => {};
+ testDone();
+});