aboutsummaryrefslogtreecommitdiff
path: root/test/regression/issue/02499.test.ts
blob: f1ee1da80afaebbf019a867348a1473b28c33716 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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 invalidJSON = Buffer.from("invalid json");

  // 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: Buffer[] = [];
        var decoder = new TextDecoder();
        while (!hostname && !port) {
          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: invalidJSON,
          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();
}, 30_000);