Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 30 additions & 19 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ function _createProxyFn<
}

let _resolve!: () => void;
let _reject: (error: any) => void;
let _reject!: (error: any) => void;
const callbackPromise = new Promise<void>((resolve, reject) => {
_resolve = resolve;
_reject = reject;
Expand All @@ -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();
Expand Down
141 changes: 141 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
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<void>((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<void>;
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<void>((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<Error>((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<void>;
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<void>((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<void>;
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<void>((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();
}
});
});
Loading