Skip to content

Commit 769957b

Browse files
author
vhess
committed
feat: Add callback functions, group options
1 parent 8c2f62c commit 769957b

File tree

4 files changed

+177
-23
lines changed

4 files changed

+177
-23
lines changed

lib/http-proxy/index.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,19 @@ export interface ServerOptions {
9393
* This is passed to https.request.
9494
*/
9595
ca?: string;
96+
/** Enable undici for HTTP/2 support. Set to true for defaults, or provide custom configuration. */
97+
undici?: boolean | UndiciOptions;
98+
}
99+
100+
export interface UndiciOptions {
101+
/** Undici Agent configuration */
96102
agentOptions?: Agent.Options;
103+
/** Undici request options */
97104
requestOptions?: Dispatcher.RequestOptions;
105+
/** Called before making the undici request */
106+
onBeforeRequest?: (requestOptions: Dispatcher.RequestOptions, req: http.IncomingMessage, res: http.ServerResponse, options: NormalizedServerOptions) => void | Promise<void>;
107+
/** Called after receiving the undici response */
108+
onAfterResponse?: (response: Dispatcher.ResponseData, req: http.IncomingMessage, res: http.ServerResponse, options: NormalizedServerOptions) => void | Promise<void>;
98109
}
99110

100111
export interface NormalizedServerOptions extends ServerOptions {
@@ -232,7 +243,7 @@ export class ProxyServer<TIncomingMessage extends typeof http.IncomingMessage =
232243
constructor(options: ServerOptions = {}) {
233244
super();
234245
log("creating a ProxyServer", options);
235-
options.prependPath = options.prependPath === false ? false : true;
246+
options.prependPath = options.prependPath !== false;
236247
this.options = options;
237248
this.web = this.createRightProxy("web")(options);
238249
this.ws = this.createRightProxy("ws")(options);

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

Lines changed: 71 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import type { Socket } from "node:net";
1717
import type Stream from "node:stream";
1818
import * as followRedirects from "follow-redirects";
1919
import { Agent, type Dispatcher, interceptors } from "undici";
20-
import type { ErrorCallback, NormalizedServerOptions, NormalizeProxyTarget, ProxyServer, ProxyTarget, ProxyTargetUrl, ServerOptions } from "..";
20+
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";
2323

@@ -79,8 +79,7 @@ export function stream(req: Request, res: Response, options: NormalizedServerOpt
7979
// And we begin!
8080
server.emit("start", req, res, options.target || options.forward!);
8181

82-
if (options.agentOptions || options.requestOptions
83-
) {
82+
if (options.undici) {
8483
return stream2(req, res, options, _, server, cb);
8584
}
8685

@@ -213,12 +212,16 @@ async function stream2(
213212
}
214213
);
215214

215+
const undiciOptions = options.undici === true ? {} as UndiciOptions : options.undici;
216+
if (!undiciOptions) {
217+
throw new Error("stream2 called without undici options");
218+
}
216219
const agentOptions: Agent.Options = {
217-
...options.agentOptions,
218220
allowH2: true,
219221
connect: {
220222
rejectUnauthorized: options.secure !== false,
221223
},
224+
...(undiciOptions.agentOptions || {}),
222225
};
223226

224227
let agent: Agent | Dispatcher = new Agent(agentOptions)
@@ -249,8 +252,36 @@ async function stream2(
249252
requestOptions.body = req;
250253
}
251254

255+
// Call onBeforeRequest callback before making the forward request
256+
if (undiciOptions.onBeforeRequest) {
257+
try {
258+
await undiciOptions.onBeforeRequest(requestOptions, req, res, options);
259+
} 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+
}
265+
return;
266+
}
267+
}
268+
252269
try {
253-
await agent.request(requestOptions)
270+
const result = await agent.request(requestOptions);
271+
272+
// Call onAfterResponse callback for forward requests (though they typically don't expect responses)
273+
if (undiciOptions.onAfterResponse) {
274+
try {
275+
await undiciOptions.onAfterResponse(result, req, res, options);
276+
} 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+
}
282+
return;
283+
}
284+
}
254285
} catch (err) {
255286
if (cb) {
256287
cb(err as Error, req, res, options.forward);
@@ -272,6 +303,7 @@ async function stream2(
272303
headers: outgoingOptions.headers || {},
273304
path: outgoingOptions.path || "/",
274305
headersTimeout: options.proxyTimeout,
306+
...undiciOptions.requestOptions
275307
};
276308

277309
if (options.auth) {
@@ -284,29 +316,47 @@ async function stream2(
284316
requestOptions.body = req;
285317
}
286318

287-
288-
289-
319+
// Call onBeforeRequest callback before making the request
320+
if (undiciOptions.onBeforeRequest) {
321+
try {
322+
await undiciOptions.onBeforeRequest(requestOptions, req, res, options);
323+
} 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+
}
329+
return;
330+
}
331+
}
332+
290333
try {
291-
const { statusCode, headers, body } = await agent.request(
292-
requestOptions
293-
);
334+
const response = await agent.request(requestOptions);
335+
336+
// Call onAfterResponse callback after receiving the response
337+
if (undiciOptions.onAfterResponse) {
338+
try {
339+
await undiciOptions.onAfterResponse(response, req, res, options);
340+
} 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+
}
346+
return;
347+
}
348+
}
349+
294350

295351
// ProxyRes is used in the outgoing passes
296352
// But since only certain properties are used, we can fake it here
297353
// to avoid having to refactor everything.
298-
const fakeProxyRes = {} as ProxyResponse;
299-
300-
fakeProxyRes.statusCode = statusCode;
301-
fakeProxyRes.headers = headers as { [key: string]: string | string[] };
302-
fakeProxyRes.rawHeaders = Object.entries(headers).flatMap(([key, value]) => {
354+
const fakeProxyRes = {...response, rawHeaders: Object.entries(response.headers).flatMap(([key, value]) => {
303355
if (Array.isArray(value)) {
304356
return value.flatMap(v => (v != null ? [key, v] : []));
305357
}
306358
return value != null ? [key, value] : [];
307-
}) as string[];
308-
fakeProxyRes.pipe = body.pipe.bind(body);
309-
359+
}) as string[]} as unknown as ProxyResponse;
310360

311361
if (!res.headersSent && !options.selfHandleResponse) {
312362
for (const pass of web_o) {
@@ -317,12 +367,12 @@ async function stream2(
317367

318368
if (!res.writableEnded) {
319369
// Allow us to listen for when the proxy has completed
320-
body.on("end", () => {
370+
response.body.on("end", () => {
321371
server?.emit("end", req, res, fakeProxyRes);
322372
});
323373
// We pipe to the response unless its expected to be handled by the user
324374
if (!options.selfHandleResponse) {
325-
body.pipe(res);
375+
response.body.pipe(res);
326376
}
327377
} else {
328378
server?.emit("end", req, res, fakeProxyRes);
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
Test the new onProxyReq and onProxyRes callbacks for undici code path
3+
4+
pnpm test proxy-callbacks.test.ts
5+
*/
6+
7+
import * as http from "node:http";
8+
import * as httpProxy from "../..";
9+
import getPort from "../get-port";
10+
import fetch from "node-fetch";
11+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
12+
import { Agent, setGlobalDispatcher } from "undici";
13+
14+
setGlobalDispatcher(new Agent({
15+
allowH2: true
16+
}));
17+
18+
describe("Undici callback functions (onBeforeRequest and onAfterResponse)", () => {
19+
let ports: Record<'target' | 'proxy', number>;
20+
const servers: Record<string, any> = {};
21+
22+
beforeAll(async () => {
23+
ports = { target: await getPort(), proxy: await getPort() };
24+
});
25+
26+
afterAll(async () => {
27+
Object.values(servers).map((x) => x?.close());
28+
});
29+
30+
it("Create the target HTTP server", async () => {
31+
servers.target = http
32+
.createServer((req, res) => {
33+
res.writeHead(200, {
34+
"Content-Type": "text/plain",
35+
"X-Target-Header": "from-target"
36+
});
37+
res.write(`Request received: ${req.method} ${req.url}\n`);
38+
res.write(`Headers: ${JSON.stringify(req.headers, null, 2)}\n`);
39+
res.end();
40+
})
41+
.listen(ports.target);
42+
});
43+
44+
it("Test onBeforeRequest and onAfterResponse callbacks", async () => {
45+
let onBeforeRequestCalled = false;
46+
let onAfterResponseCalled = false;
47+
let capturedResponse: unknown = {};
48+
49+
const proxy = httpProxy.createServer({
50+
target: `http://localhost:${ports.target}`,
51+
undici: {
52+
agentOptions: { allowH2: true }, // Enable undici code path
53+
onBeforeRequest: async (requestOptions, _req, _res, _options) => {
54+
onBeforeRequestCalled = true;
55+
// Modify the outgoing request
56+
requestOptions.headers = {
57+
...requestOptions.headers,
58+
'X-Proxy-Added': 'callback-added-header',
59+
'X-Original-Method': _req.method || 'unknown'
60+
};
61+
},
62+
onAfterResponse: async (response, _req, _res, _options) => {
63+
onAfterResponseCalled = true;
64+
capturedResponse = response;
65+
console.log(`Response received: ${response.statusCode}`);
66+
}
67+
}
68+
}); servers.proxy = proxy.listen(ports.proxy);
69+
70+
// Make a request through the proxy
71+
const response = await fetch(`http://localhost:${ports.proxy}/test`);
72+
const text = await response.text();
73+
74+
// Check that the response is successful
75+
expect(response.status).toBe(200);
76+
expect(text).toContain("Request received: GET /test");
77+
78+
// Check that our added header made it to the target
79+
expect(text).toContain("x-proxy-added");
80+
expect(text).toContain("callback-added-header");
81+
82+
// Check that callbacks were called
83+
expect(onBeforeRequestCalled).toBe(true);
84+
expect(onAfterResponseCalled).toBe(true);
85+
86+
// Check that we received the full response object
87+
expect(capturedResponse).toHaveProperty('statusCode');
88+
expect((capturedResponse as { statusCode: number }).statusCode).toBe(200);
89+
expect(capturedResponse).toHaveProperty('headers');
90+
expect((capturedResponse as { headers: Record<string, string> }).headers).toHaveProperty('x-target-header');
91+
expect((capturedResponse as { headers: Record<string, string> }).headers['x-target-header']).toBe('from-target');
92+
});
93+
});

lib/test/http/proxy-http2-to-http2.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ describe("Basic example of proxying over HTTP2 to a target HTTP2 server", () =>
4646
.createServer({
4747
target: `https://localhost:${ports.http2}`,
4848
ssl,
49-
agentOptions: { allowH2: true },
49+
undici: { agentOptions: { allowH2: true } },
5050
// without secure false, clients will fail and this is broken:
5151
secure: false,
5252
})

0 commit comments

Comments
 (0)