diff --git a/src/tools/web-tools.ts b/src/tools/web-tools.ts index b4850ca..7945f65 100644 --- a/src/tools/web-tools.ts +++ b/src/tools/web-tools.ts @@ -748,9 +748,9 @@ export function collectRawHttpResponse(url: string, socket: import("node:net").S let settled = false; const cleanup = () => { socket.removeListener("data", onData); - socket.removeListener("error", onError); socket.removeListener("end", onEnd); - socket.removeListener("close", onClose); + // TLS can emit an error after end (for example during a failed handshake). + // Keep handling errors until close, even when the response has settled. }; const finishResolve = (value: Response) => { if (settled) return; @@ -849,9 +849,12 @@ export function collectRawHttpResponse(url: string, socket: import("node:net").S finishReject(error); } }; - const onClose = () => finishReject(new Error("Proxy HTTPS response socket closed before the response ended")); + const onClose = () => { + finishReject(new Error("Proxy HTTPS response socket closed before the response ended")); + socket.removeListener("error", onError); + }; socket.on("data", onData); - socket.once("error", onError); + socket.on("error", onError); socket.once("end", onEnd); socket.once("close", onClose); }); diff --git a/tests/unit/web-proxy-tls.test.ts b/tests/unit/web-proxy-tls.test.ts new file mode 100644 index 0000000..0cd06c7 --- /dev/null +++ b/tests/unit/web-proxy-tls.test.ts @@ -0,0 +1,27 @@ +import assert from "node:assert/strict"; +import net from "node:net"; +import tls from "node:tls"; +import { once } from "node:events"; +import test from "node:test"; +import { collectRawHttpResponse } from "../../src/tools/web-tools.ts"; + +test("HTTPS proxy handshake disconnect rejects without an unhandled TLS error", async () => { + const server = net.createServer((socket) => { + socket.resume(); + socket.end(); + }); + server.listen(0, "127.0.0.1"); + await once(server, "listening"); + const address = server.address() as net.AddressInfo; + const socket = tls.connect({ host: "127.0.0.1", port: address.port, servername: "localhost" }); + const closed = new Promise((resolve) => socket.once("close", () => resolve())); + try { + await assert.rejects(collectRawHttpResponse("https://localhost/", socket), + /headers were complete|TLS connection|socket closed/); + await closed; + assert.equal(socket.listenerCount("error"), 0); + } finally { + socket.destroy(); + await new Promise((resolve) => server.close(() => resolve())); + } +});