diff --git a/express-zod-api/package.json b/express-zod-api/package.json index b63f4d3bf..680528fb3 100644 --- a/express-zod-api/package.json +++ b/express-zod-api/package.json @@ -102,7 +102,6 @@ "node-forge": "^1.3.3", "snakify-ts": "^2.3.0", "typescript": "catalog:dev", - "undici": "^7.16.0", "zod": "catalog:dev" }, "keywords": [ diff --git a/express-zod-api/tests/graceful-shutdown.spec.ts b/express-zod-api/tests/graceful-shutdown.spec.ts index b8b5a275d..e1a7a649f 100644 --- a/express-zod-api/tests/graceful-shutdown.spec.ts +++ b/express-zod-api/tests/graceful-shutdown.spec.ts @@ -1,13 +1,20 @@ import assert from "node:assert/strict"; import http from "node:http"; import https from "node:https"; -import { Agent, fetch } from "undici"; import { setTimeout } from "node:timers/promises"; import { monitor } from "../src/graceful-shutdown"; import { givePort } from "../../tools/ports"; import { signCert } from "./ssl-helpers"; +interface HttpResult { + res: http.IncomingMessage; + body: string; + headers: http.IncomingHttpHeaders; +} + describe("monitor()", () => { + const sslOptions = signCert(); + const makeHttpServer = (handler: http.RequestListener) => { const { promise, resolve } = Promise.withResolvers<[http.Server, number]>(); const subject = http.createServer(handler); @@ -19,7 +26,7 @@ describe("monitor()", () => { const makeHttpsServer = (handler: http.RequestListener) => { const { promise, resolve } = Promise.withResolvers<[https.Server, number]>(); - const subject = https.createServer(signCert(), handler); + const subject = https.createServer(sslOptions, handler); const port = givePort(); subject.listen(port, () => resolve([subject, port])); return promise; @@ -31,6 +38,47 @@ describe("monitor()", () => { return promise; }; + const handleResponse = ( + resolve: (value: HttpResult) => void, + res: http.IncomingMessage, + ) => { + const chunks: Buffer[] = []; + res.on("data", (chunk) => chunks.push(chunk)); + res.on("end", () => + resolve({ + res, + body: Buffer.concat(chunks).toString(), + headers: res.headers, + }), + ); + }; + + const makeHttpRequest = ( + port: number, + options?: http.RequestOptions, + ): Promise => { + const { promise, resolve, reject } = Promise.withResolvers(); + const req = http.request({ ...options, port }, (res) => + handleResponse(resolve, res), + ); + req.on("error", reject); + req.end(); + return promise; + }; + + const makeHttpsRequest = ( + port: number, + options?: https.RequestOptions, + ): Promise => { + const { promise, resolve, reject } = Promise.withResolvers(); + const req = https.request({ ...sslOptions, ...options, port }, (res) => + handleResponse(resolve, res), + ); + req.on("error", reject); + req.end(); + return promise; + }; + test( "shuts down HTTP server with no connections", { timeout: 100 }, @@ -50,9 +98,9 @@ describe("monitor()", () => { const handler = vi.fn(); const [httpServer, port] = await makeHttpServer(handler); const graceful = monitor([httpServer], { timeout: 150 }); - void fetch(`http://localhost:${port}`, { - headers: { connection: "close" }, - }).catch(vi.fn()); + makeHttpRequest(port, { headers: { connection: "close" } }).catch( + vi.fn(), + ); await vi.waitFor(() => assert(handler.mock.calls.length === 1), { interval: 30, // unstable }); @@ -75,19 +123,19 @@ describe("monitor()", () => { res.end("foo"); }); const graceful = monitor([httpServer], { timeout: 150 }); - const request0 = fetch(`http://localhost:${port}`, { + const request0 = makeHttpRequest(port, { headers: { connection: "close" }, }); await setTimeout(50); void graceful.shutdown(); await setTimeout(50); - const request1 = fetch(`http://localhost:${port}`, { + const request1 = makeHttpRequest(port, { headers: { connection: "close" }, }); await expect(request1).rejects.toThrowError(); const response0 = await request0; - expect(response0.headers.get("connection")).toBe("close"); - await expect(response0.text()).resolves.toBe("foo"); + expect(response0.headers.connection).toBe("close"); + expect(response0.body).toBe("foo"); }, ); @@ -100,12 +148,14 @@ describe("monitor()", () => { res.end("foo"); }); const graceful = monitor([httpServer], { timeout: 150 }); - const request = fetch(`http://localhost:${port}`, { keepalive: true }); + const request = makeHttpRequest(port, { + headers: { connection: "keep-alive" }, + }); await setTimeout(50); void graceful.shutdown(); const response = await request; - expect(response.headers.get("connection")).toBe("close"); - await expect(response.text()).resolves.toBe("foo"); + expect(response.headers.connection).toBe("close"); + expect(response.body).toBe("foo"); }, ); @@ -126,19 +176,20 @@ describe("monitor()", () => { }); const [httpServer, port] = await makeHttpServer(handler); const graceful = monitor([httpServer], { timeout: 150 }); - const dispatcher = new Agent({ pipelining: 5, keepAliveTimeout: 5e3 }); - const request0 = fetch(`http://localhost:${port}`, { dispatcher }); + const agent = new http.Agent({ keepAlive: true, maxSockets: 1 }); + const request0 = makeHttpRequest(port, { agent }); await setTimeout(50); void graceful.shutdown(); - const request1 = fetch(`http://localhost:${port}`, { dispatcher }); + const request1 = makeHttpRequest(port, { agent }); await setTimeout(50); expect(handler).toHaveBeenCalledTimes(2); const response0 = await request0; - expect(response0.headers.get("connection")).toBe("keep-alive"); - await expect(response0.text()).resolves.toBe("foobar"); + expect(response0.headers.connection).toBe("keep-alive"); + expect(response0.body).toBe("foobar"); const response1 = await request1; - expect(response1.headers.get("connection")).toBe("close"); - await expect(response1.text()).resolves.toBe("baz"); + expect(response1.headers.connection).toBe("close"); + expect(response1.body).toBe("baz"); + agent.destroy(); }, ); @@ -147,9 +198,7 @@ describe("monitor()", () => { res.end("foo"); }); const graceful = monitor([httpServer], { timeout: 150 }); - await fetch(`http://localhost:${port}`, { - headers: { connection: "close" }, - }); + await makeHttpRequest(port, { headers: { connection: "close" } }); await setTimeout(50); expect(graceful.sockets.size).toBe(0); await graceful.shutdown(); @@ -165,10 +214,7 @@ describe("monitor()", () => { { timeout: 500 }, async () => { const graceful = monitor([httpsServer], { timeout: 150 }); - await fetch(`https://localhost:${port}`, { - dispatcher: new Agent({ connect: { rejectUnauthorized: false } }), - headers: { connection: "close" }, - }); + await makeHttpsRequest(port, { headers: { connection: "close" } }); await setTimeout(50); expect(graceful.sockets.size).toBe(0); await graceful.shutdown(); @@ -187,9 +233,9 @@ describe("monitor()", () => { const [httpServer, port] = await makeHttpServer(spy); expect(httpServer.listening).toBeTruthy(); const graceful = monitor([httpServer], { timeout: 500 }); - void fetch(`http://localhost:${port}`, { - headers: { connection: "close" }, - }); + makeHttpRequest(port, { headers: { connection: "close" } }).catch( + vi.fn(), + ); await setTimeout(50); await expect(getConnections(httpServer)).resolves.toBe(1); void graceful.shutdown(); diff --git a/express-zod-api/tests/ssl-helpers.ts b/express-zod-api/tests/ssl-helpers.ts index ea217850b..8304a1861 100644 --- a/express-zod-api/tests/ssl-helpers.ts +++ b/express-zod-api/tests/ssl-helpers.ts @@ -33,8 +33,10 @@ export const signCert = () => { cert.setIssuer(certAttr); cert.setExtensions(certExt); cert.sign(keys.privateKey, forge.md.sha256.create()); + const certPem = forge.pki.certificateToPem(cert); return { - cert: forge.pki.certificateToPem(cert), + ca: certPem, + cert: certPem, key: forge.pki.privateKeyToPem(keys.privateKey), }; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0304203cb..c26d3f83e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -255,9 +255,6 @@ importers: typescript: specifier: catalog:dev version: 6.0.2 - undici: - specifier: ^7.16.0 - version: 7.24.7 zod: specifier: catalog:dev version: 4.3.6 @@ -1670,10 +1667,6 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} - undici@7.24.7: - resolution: {integrity: sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==} - engines: {node: '>=20.18.1'} - unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -3274,8 +3267,6 @@ snapshots: undici-types@7.18.2: {} - undici@7.24.7: {} - unpipe@1.0.0: {} unrun@0.2.34(synckit@0.11.12): diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0ced99b8b..162c8266a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -34,7 +34,6 @@ minimumReleaseAgeExclude: - typescript - typescript-eslint - tsdown - - undici - unrun - vite - vitest