diff options
Diffstat (limited to 'test')
-rw-r--r-- | test/regression/issue/02499-repro.ts | 21 | ||||
-rw-r--r-- | test/regression/issue/02499.test.ts | 95 |
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(); +}); |