Files
bun.sh/test/js/web/fetch/fetch-leak.test.ts
2024-12-27 14:07:41 -08:00

185 lines
4.4 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tls as COMMON_CERT, gc } from "harness";
import { once } from "node:events";
import { createServer } from "node:http";
import { join } from "node:path";
describe("fetch doesn't leak", () => {
test("fixture #1", async () => {
const body = new Blob(["some body in here!".repeat(100)]);
var count = 0;
using server = Bun.serve({
port: 0,
idleTimeout: 0,
fetch(req) {
count++;
return new Response(body);
},
});
const proc = Bun.spawn({
env: {
...bunEnv,
SERVER: server.url.href,
COUNT: "200",
},
stderr: "inherit",
stdout: "inherit",
cmd: [bunExe(), "--smol", join(import.meta.dir, "fetch-leak-test-fixture.js")],
});
const exitCode = await proc.exited;
expect(exitCode).toBe(0);
expect(count).toBe(200);
});
// This tests for body leakage and Response object leakage.
async function runTest(compressed, name) {
const body = !compressed
? new Blob(["some body in here!".repeat(2000000)])
: new Blob([Bun.deflateSync(crypto.getRandomValues(new Buffer(65123)))]);
const tls = name.includes("tls");
const headers = {
"Content-Type": "application/octet-stream",
};
if (compressed) {
headers["Content-Encoding"] = "deflate";
}
const serveOptions = {
port: 0,
idleTimeout: 0,
fetch(req) {
return new Response(body, { headers });
},
};
if (tls) {
serveOptions.tls = { ...COMMON_CERT };
}
using server = Bun.serve(serveOptions);
const env = {
...bunEnv,
SERVER: server.url.href,
BUN_JSC_forceRAMSize: (1024 * 1024 * 64).toString(10),
NAME: name,
};
if (tls) {
env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
}
if (compressed) {
env.COUNT = "1000";
}
const proc = Bun.spawn({
env,
stderr: "inherit",
stdout: "inherit",
cmd: [bunExe(), "--smol", join(import.meta.dir, "fetch-leak-test-fixture-2.js")],
});
const exitCode = await proc.exited;
expect(exitCode).toBe(0);
}
for (let compressed of [true, false]) {
describe(compressed ? "compressed" : "uncompressed", () => {
for (let name of ["tcp", "tls", "tls-with-client"]) {
describe(name, () => {
test("fixture #2", async () => {
await runTest(compressed, name);
}, 100000);
});
}
});
}
});
describe.each(["FormData", "Blob", "Buffer", "String", "URLSearchParams", "stream", "iterator"])("Sending %s", type => {
test(
"does not leak",
async () => {
using server = Bun.serve({
port: 0,
idleTimeout: 0,
fetch(req) {
return new Response();
},
});
const rss = [];
const process = Bun.spawn({
cmd: [
bunExe(),
"--smol",
join(import.meta.dir, "fetch-leak-test-fixture-5.js"),
server.url.href,
1024 * 1024 * 2 + "",
type,
],
stdin: "ignore",
stdout: "inherit",
stderr: "inherit",
env: {
...bunEnv,
},
ipc(message) {
rss.push(message.rss);
},
});
await process.exited;
const first = rss[0];
const last = rss[rss.length - 1];
console.log({ rss, delta: (((last - first) / 1024 / 1024) | 0) + " MB" });
expect(last).toBeLessThan(first * 10);
},
20 * 1000,
);
});
test("do not leak", async () => {
await using server = createServer((req, res) => {
res.end();
}).listen(0);
await once(server, "listening");
let url;
let isDone = false;
server.listen(0, "127.0.0.1", function attack() {
if (isDone) {
return;
}
url ??= new URL(`http://127.0.0.1:${server.address().port}`);
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(res => res.arrayBuffer())
.catch(() => {})
.then(attack);
});
let prev = Infinity;
let count = 0;
var interval = setInterval(() => {
isDone = true;
gc();
const next = process.memoryUsage().heapUsed;
if (next <= prev) {
expect(true).toBe(true);
clearInterval(interval);
} else if (count++ > 20) {
clearInterval(interval);
expect.unreachable();
} else {
prev = next;
}
}, 1e3);
});