Files
openwork/apps/server/src/serve-node.test.ts
T

252 lines
7.5 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { EventEmitter } from "node:events";
import type { ServerResponse } from "node:http";
import { connect } from "node:net";
import { setTimeout as delay } from "node:timers/promises";
import { serve, writeWebResponse } from "./serve-node.js";
describe("serve", () => {
test("handles a malformed raw Node TRACE request without an unhandled rejection", async () => {
const unhandled: unknown[] = [];
const onUnhandledRejection = (error: unknown) => {
unhandled.push(error);
};
process.on("unhandledRejection", onUnhandledRejection);
let fetchCalls = 0;
const server = await serve({
hostname: "127.0.0.1",
port: 0,
fetch: () => {
fetchCalls += 1;
return Response.json({ ok: true });
},
});
try {
const response = await new Promise<string>((resolve, reject) => {
let received = "";
const socket = connect({ host: "127.0.0.1", port: server.port }, () => {
socket.write([
"TRACE http://example.com/ HTTP/1.1",
`Host: 127.0.0.1:${server.port}`,
"Connection: close",
"",
"",
].join("\r\n"));
});
const timeout = setTimeout(() => {
socket.destroy(new Error("Timed out waiting for the TRACE response"));
}, 1_000);
socket.setEncoding("utf8");
socket.on("data", (chunk) => {
received += chunk;
});
socket.once("end", () => {
clearTimeout(timeout);
resolve(received);
});
socket.once("error", (error) => {
clearTimeout(timeout);
reject(error);
});
});
await delay(25);
expect(response).toContain("HTTP/1.1 500 Internal Server Error");
expect(response).toEndWith(JSON.stringify({ error: "internal_error" }));
expect(fetchCalls).toBe(0);
expect(unhandled).toEqual([]);
} finally {
process.off("unhandledRejection", onUnhandledRejection);
await server.stop();
}
});
test("cancels a streaming response body when the client disconnects", async () => {
let cancelled = false;
const events = new EventEmitter();
const responseState = {
destroyed: false,
closed: false,
writableEnded: false,
writeHead: () => undefined,
write: () => true,
end: () => undefined,
once: events.once.bind(events),
off: events.off.bind(events),
} as unknown as ServerResponse;
const response = new Response(new ReadableStream<Uint8Array>({
async pull(controller) {
await delay(5);
controller.enqueue(new TextEncoder().encode("event\n"));
},
cancel() {
cancelled = true;
},
}));
const writing = writeWebResponse(response, responseState);
await delay(20);
Object.assign(responseState, { closed: true });
events.emit("close");
await writing;
expect(cancelled).toBe(true);
});
test("does not write an error response after a streaming response has ended", async () => {
const uncaught: unknown[] = [];
const onUncaughtException = (error: unknown) => {
uncaught.push(error);
};
process.on("uncaughtException", onUncaughtException);
const encoder = new TextEncoder();
const server = await serve({
hostname: "127.0.0.1",
port: 0,
fetch: (request) => {
if (new URL(request.url).pathname === "/health") {
return Response.json({ ok: true });
}
let wroteChunk = false;
return new Response(
new ReadableStream<Uint8Array>({
pull(controller) {
if (!wroteChunk) {
wroteChunk = true;
controller.enqueue(encoder.encode("partial"));
return;
}
controller.error(new Error("stream failed after response started"));
},
}),
);
},
});
try {
const response = await fetch(`http://127.0.0.1:${server.port}/stream`);
await response.text().catch(() => undefined);
await delay(25);
expect(uncaught).toEqual([]);
const health = await fetch(`http://127.0.0.1:${server.port}/health`);
expect(health.status).toBe(200);
expect(await health.json()).toEqual({ ok: true });
} finally {
process.off("uncaughtException", onUncaughtException);
await server.stop();
}
});
test("awaits shutdown before resolving stop", async () => {
const first = await serve({
hostname: "127.0.0.1",
port: 0,
fetch: () => Response.json({ ok: true }),
});
const port = first.port;
await first.stop();
const second = await serve({
hostname: "127.0.0.1",
port,
fetch: () => Response.json({ ok: true }),
});
expect(second.port).toBe(port);
await second.stop();
});
test("reuses the in-flight shutdown for repeated stop calls", async () => {
const first = await serve({
hostname: "127.0.0.1",
port: 0,
fetch: () => Response.json({ ok: true }),
});
const port = first.port;
await Promise.all([first.stop(), first.stop()]);
const second = await serve({
hostname: "127.0.0.1",
port,
fetch: () => Response.json({ ok: true }),
});
expect(second.port).toBe(port);
await second.stop();
});
test("does not log expected connection aborts as unhandled errors", async () => {
const errors: unknown[] = [];
const originalError = console.error;
console.error = (...args: unknown[]) => {
errors.push(args);
};
const server = await serve({
hostname: "127.0.0.1",
port: 0,
fetch: (request) => {
if (new URL(request.url).pathname === "/health") {
return Response.json({ ok: true });
}
throw new TypeError("terminated", { cause: { code: "UND_ERR_SOCKET" } });
},
});
try {
await fetch(`http://127.0.0.1:${server.port}/abort`).catch(() => undefined);
await delay(25);
expect(errors).toEqual([]);
const health = await fetch(`http://127.0.0.1:${server.port}/health`);
expect(health.status).toBe(200);
expect(await health.json()).toEqual({ ok: true });
} finally {
console.error = originalError;
await server.stop();
}
});
test("aborts the Web request signal when the client cancels", async () => {
let markStarted: () => void = () => undefined;
const started = new Promise<void>((resolve) => {
markStarted = resolve;
});
let observedAbort: unknown;
const server = await serve({
hostname: "127.0.0.1",
port: 0,
fetch: (request) => new Promise<Response>((_resolve, reject) => {
markStarted();
const onAbort = () => {
observedAbort = request.signal.reason;
reject(request.signal.reason);
};
if (request.signal.aborted) onAbort();
else request.signal.addEventListener("abort", onAbort, { once: true });
}),
});
const controller = new AbortController();
const pending = fetch(`http://127.0.0.1:${server.port}/cancel`, { signal: controller.signal });
try {
await started;
controller.abort();
await pending.catch(() => undefined);
for (let attempt = 0; attempt < 20 && observedAbort === undefined; attempt += 1) {
await delay(10);
}
expect(observedAbort).toBeInstanceOf(DOMException);
expect(observedAbort).toMatchObject({ name: "AbortError" });
} finally {
await server.stop();
}
});
});