diff --git a/src/server.ts b/src/server.ts index 8c13afd..aed7cef 100644 --- a/src/server.ts +++ b/src/server.ts @@ -208,7 +208,7 @@ function _createProxyFn< } let _resolve!: () => void; - let _reject: (error: any) => void; + let _reject!: (error: any) => void; const callbackPromise = new Promise((resolve, reject) => { _resolve = resolve; _reject = reject; @@ -222,24 +222,35 @@ function _createProxyFn< }); for (const pass of server._getPasses(type)) { - const stop = pass( - req, - res, - requestOptions as ProxyServerOptions & { target: URL; forward: URL }, - server as ProxyServer< - http.IncomingMessage | http2.Http2ServerRequest, - http.ServerResponse | http2.Http2ServerResponse - >, - head, - (error) => { - if (server.listenerCount("error") > 0) { - server.emit("error", error, req, res as ProxyServerRes | net.Socket); - _resolve(); - } else { - _reject(error); - } - }, - ); + let stop: void | true; + try { + stop = pass( + req, + res, + requestOptions as ProxyServerOptions & { target: URL; forward: URL }, + server as ProxyServer< + http.IncomingMessage | http2.Http2ServerRequest, + http.ServerResponse | http2.Http2ServerResponse + >, + head, + (error) => { + if (server.listenerCount("error") > 0) { + server.emit("error", error, req, res as ProxyServerRes | net.Socket); + _resolve(); + } else { + _reject(error); + } + }, + ); + } catch (error) { + if (server.listenerCount("error") > 0) { + server.emit("error", error as Error, req, res as ProxyServerRes | net.Socket); + _resolve(); + } else { + _reject(error); + } + break; + } // Passes can return a truthy value to halt the loop if (stop) { _resolve(); diff --git a/test/index.test.ts b/test/index.test.ts index fcbca12..dc9e726 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -106,3 +106,144 @@ describe("httpxy", () => { expect(lastRejected).toBe(undefined); }); }); + +describe("middleware pass exceptions", () => { + it("should forward synchronous pass errors to error event", async () => { + const target = await new Promise<{ + close: () => Promise; + url: string; + }>((resolve, reject) => { + const server = http.createServer((_req, res) => { + res.end("ok"); + }); + server.once("error", reject); + server.listen(0, "127.0.0.1", () => { + const { port } = server.address() as AddressInfo; + resolve({ + close: () => + new Promise((r, j) => { + server.close((e) => (e ? j(e) : r())); + }), + url: `http://127.0.0.1:${port}/`, + }); + }); + }); + + const proxy = createProxyServer({ target: target.url }); + + // Inject a middleware pass that throws synchronously (simulates ERR_INVALID_HTTP_TOKEN) + const testError = new TypeError("Invalid character in header"); + proxy.before("web", "", () => { + throw testError; + }); + + const proxyServer = await new Promise<{ + close: () => Promise; + url: string; + }>((resolve, reject) => { + const server = http.createServer((req, res) => { + void proxy.web(req, res); + }); + server.once("error", reject); + server.listen(0, "127.0.0.1", () => { + const { port } = server.address() as AddressInfo; + resolve({ + close: () => + new Promise((r, j) => { + server.close((e) => (e ? j(e) : r())); + }), + url: `http://127.0.0.1:${port}/`, + }); + }); + }); + + try { + // With an error listener, the error should be emitted, not thrown + const errorPromise = new Promise((resolve) => { + proxy.on("error", (err, _req, res) => { + resolve(err); + // End the response so the request doesn't hang + if (res && "writeHead" in res && !res.headersSent) { + res.writeHead(502); + res.end(); + } + }); + }); + + // The request may fail since the proxy errored before sending a response + await $fetch(proxyServer.url, { ignoreResponseError: true }).catch(() => {}); + + const emittedError = await errorPromise; + expect(emittedError).toBe(testError); + } finally { + proxy.close(); + await proxyServer.close(); + await target.close(); + } + }); + + it("should reject promise when no error listener and pass throws", async () => { + const target = await new Promise<{ + close: () => Promise; + url: string; + }>((resolve, reject) => { + const server = http.createServer((_req, res) => { + res.end("ok"); + }); + server.once("error", reject); + server.listen(0, "127.0.0.1", () => { + const { port } = server.address() as AddressInfo; + resolve({ + close: () => + new Promise((r, j) => { + server.close((e) => (e ? j(e) : r())); + }), + url: `http://127.0.0.1:${port}/`, + }); + }); + }); + + const proxy = createProxyServer({ target: target.url }); + + // Inject a middleware pass that throws synchronously + const testError = new TypeError("Invalid character in header"); + proxy.before("web", "", () => { + throw testError; + }); + + const proxyServer = await new Promise<{ + close: () => Promise; + url: string; + }>((resolve, reject) => { + const server = http.createServer((req, res) => { + void proxy.web(req, res).catch(() => { + res.statusCode = 502; + res.end("error"); + }); + }); + server.once("error", reject); + server.listen(0, "127.0.0.1", () => { + const { port } = server.address() as AddressInfo; + resolve({ + close: () => + new Promise((r, j) => { + server.close((e) => (e ? j(e) : r())); + }), + url: `http://127.0.0.1:${port}/`, + }); + }); + }); + + try { + // No error listener - the promise should reject with the thrown error + const response = await $fetch.raw(proxyServer.url, { + ignoreResponseError: true, + }); + expect(response.status).toBe(502); + } finally { + proxy.close(); + await proxyServer.close(); + await target.close(); + } + }); +});