Skip to content

Commit 94c72bb

Browse files
author
vhess
committed
unify error handling and reuse agents
1 parent e8d0504 commit 94c72bb

File tree

2 files changed

+66
-55
lines changed

2 files changed

+66
-55
lines changed

lib/http-proxy/index.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { Stream } from "node:stream";
88
import debug from "debug";
99
import { toURL } from "./common";
1010
import type { Agent, Dispatcher } from "undici";
11+
import { Agent as UndiciAgent, interceptors } from "undici";
1112

1213
const log = debug("http-proxy-3");
1314

@@ -236,6 +237,9 @@ export class ProxyServer<TIncomingMessage extends typeof http.IncomingMessage =
236237
private wsPasses: Array<PassFunctions<TIncomingMessage, TServerResponse, TError>['ws']>;
237238
private _server?: http.Server<TIncomingMessage, TServerResponse> | http2.Http2SecureServer<TIncomingMessage, TServerResponse> | null;
238239

240+
// Undici agent for this proxy server
241+
public undiciAgent?: Agent;
242+
239243
/**
240244
* Creates the proxy server with specified options.
241245
* @param options - Config object passed to the proxy
@@ -250,6 +254,33 @@ export class ProxyServer<TIncomingMessage extends typeof http.IncomingMessage =
250254
this.webPasses = Object.values(WEB_PASSES) as Array<PassFunctions<TIncomingMessage, TServerResponse, TError>['web']>;
251255
this.wsPasses = Object.values(WS_PASSES) as Array<PassFunctions<TIncomingMessage, TServerResponse, TError>['ws']>;
252256
this.on("error", this.onError);
257+
258+
// Initialize undici agent if enabled
259+
if (options.undici) {
260+
this.initializeAgent(options.undici);
261+
}
262+
}
263+
264+
/**
265+
* Initialize the single undici agent based on server options
266+
*/
267+
private initializeAgent(undiciOptions: UndiciOptions | boolean): void {
268+
const resolvedOptions = undiciOptions === true ? {} as UndiciOptions : undiciOptions as UndiciOptions;
269+
const agentOptions: Agent.Options = {
270+
allowH2: true,
271+
connect: {
272+
rejectUnauthorized: this.options.secure !== false,
273+
},
274+
...(resolvedOptions.agentOptions || {}),
275+
};
276+
277+
this.undiciAgent = new UndiciAgent(agentOptions);
278+
279+
if (this.options.followRedirects) {
280+
this.undiciAgent = this.undiciAgent.compose(
281+
interceptors.redirect({ maxRedirections: 5 })
282+
) as Agent;
283+
}
253284
}
254285

255286
/**

lib/http-proxy/passes/web-incoming.ts

Lines changed: 35 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import * as https from "node:https";
1616
import type { Socket } from "node:net";
1717
import type Stream from "node:stream";
1818
import * as followRedirects from "follow-redirects";
19-
import { Agent, type Dispatcher, interceptors } from "undici";
19+
import type { Dispatcher } from "undici";
2020
import type { ErrorCallback, NormalizedServerOptions, NormalizeProxyTarget, ProxyServer, ProxyTarget, ProxyTargetUrl, ServerOptions, UndiciOptions } from "..";
2121
import * as common from "../common";
2222
import { type EditableResponse, OUTGOING_PASSES } from "./web-outgoing";
@@ -199,35 +199,37 @@ async function stream2(
199199
cb?: ErrorCallback,
200200
) {
201201

202+
// Helper function to handle errors consistently throughout the undici path
203+
// Centralizes the error handling logic to avoid repetition
204+
const handleError = (err: Error, target?: ProxyTargetUrl) => {
205+
if (cb) {
206+
cb(err, req, res, target);
207+
} else {
208+
server.emit("error", err, req, res, target);
209+
}
210+
};
211+
202212
req.on("error", (err: Error) => {
203213
if (req.socket.destroyed && (err as NodeJS.ErrnoException).code === "ECONNRESET") {
204-
server.emit("econnreset", err, req, res, options.target || options.forward!);
214+
const target = options.target || options.forward;
215+
if (target) {
216+
server.emit("econnreset", err, req, res, target);
217+
}
205218
return;
206219
}
207-
if (cb) {
208-
cb(err, req, res);
209-
} else {
210-
server.emit("error", err, req, res);
211-
}
212-
}
213-
);
220+
handleError(err);
221+
});
214222

215223
const undiciOptions = options.undici === true ? {} as UndiciOptions : options.undici;
216224
if (!undiciOptions) {
217225
throw new Error("stream2 called without undici options");
218226
}
219-
const agentOptions: Agent.Options = {
220-
allowH2: true,
221-
connect: {
222-
rejectUnauthorized: options.secure !== false,
223-
},
224-
...(undiciOptions.agentOptions || {}),
225-
};
226227

227-
let agent: Agent | Dispatcher = new Agent(agentOptions)
228+
const agent = server.undiciAgent
228229

229-
if (options.followRedirects) {
230-
agent = agent.compose(interceptors.redirect({ maxRedirections: 5 }))
230+
if (!agent) {
231+
handleError(new Error("Undici agent not initialized"));
232+
return;
231233
}
232234

233235
if (options.forward) {
@@ -257,11 +259,7 @@ async function stream2(
257259
try {
258260
await undiciOptions.onBeforeRequest(requestOptions, req, res, options);
259261
} catch (err) {
260-
if (cb) {
261-
cb(err as Error, req, res, options.forward);
262-
} else {
263-
server.emit("error", err as Error, req, res, options.forward);
264-
}
262+
handleError(err as Error, options.forward);
265263
return;
266264
}
267265
}
@@ -274,20 +272,12 @@ async function stream2(
274272
try {
275273
await undiciOptions.onAfterResponse(result, req, res, options);
276274
} catch (err) {
277-
if (cb) {
278-
cb(err as Error, req, res, options.forward);
279-
} else {
280-
server.emit("error", err as Error, req, res, options.forward);
281-
}
275+
handleError(err as Error, options.forward);
282276
return;
283277
}
284278
}
285279
} catch (err) {
286-
if (cb) {
287-
cb(err as Error, req, res, options.forward);
288-
} else {
289-
server.emit("error", err as Error, req, res, options.forward);
290-
}
280+
handleError(err as Error, options.forward);
291281
}
292282

293283
if (!options.target) {
@@ -321,11 +311,7 @@ async function stream2(
321311
try {
322312
await undiciOptions.onBeforeRequest(requestOptions, req, res, options);
323313
} catch (err) {
324-
if (cb) {
325-
cb(err as Error, req, res, options.target);
326-
} else {
327-
server.emit("error", err as Error, req, res, options.target);
328-
}
314+
handleError(err as Error, options.target);
329315
return;
330316
}
331317
}
@@ -338,11 +324,7 @@ async function stream2(
338324
try {
339325
await undiciOptions.onAfterResponse(response, req, res, options);
340326
} catch (err) {
341-
if (cb) {
342-
cb(err as Error, req, res, options.target);
343-
} else {
344-
server.emit("error", err as Error, req, res, options.target);
345-
}
327+
handleError(err as Error, options.target);
346328
return;
347329
}
348330
}
@@ -351,12 +333,14 @@ async function stream2(
351333
// ProxyRes is used in the outgoing passes
352334
// But since only certain properties are used, we can fake it here
353335
// to avoid having to refactor everything.
354-
const fakeProxyRes = {...response, rawHeaders: Object.entries(response.headers).flatMap(([key, value]) => {
355-
if (Array.isArray(value)) {
356-
return value.flatMap(v => (v != null ? [key, v] : []));
357-
}
358-
return value != null ? [key, value] : [];
359-
}) as string[]} as unknown as ProxyResponse;
336+
const fakeProxyRes = {
337+
...response, rawHeaders: Object.entries(response.headers).flatMap(([key, value]) => {
338+
if (Array.isArray(value)) {
339+
return value.flatMap(v => (v != null ? [key, v] : []));
340+
}
341+
return value != null ? [key, value] : [];
342+
}) as string[]
343+
} as unknown as ProxyResponse;
360344

361345
if (!res.headersSent && !options.selfHandleResponse) {
362346
for (const pass of web_o) {
@@ -381,11 +365,7 @@ async function stream2(
381365

382366
} catch (err) {
383367
if (err) {
384-
if (cb) {
385-
cb(err as Error, req, res, options.target);
386-
} else {
387-
server.emit("error", err as Error, req, res, options.target);
388-
}
368+
handleError(err as Error, options.target);
389369
}
390370
}
391371

0 commit comments

Comments
 (0)