@openclaw/proxyline 0.3.7 → 0.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.8 - 2026-08-31
4
+
5
+ - Fixed HTTP-forward Node agents to enforce the default 30-second proxy handshake timeout and cancel pending sockets when callers abort or destroy requests; explicit `0` remains unbounded. (#27) Thanks @SebTardif.
6
+ - Fixed Node proxy request hooks to follow the request selected by finite connection pools across origins, keeping queued requests' timeouts and cancellation independent. (#27)
7
+ - Hardened ambient `NO_PROXY` hostname normalization against dot-heavy input, docs table-of-contents text extraction, and proxy/TLS test isolation. (#24) Thanks @vincentkoc.
8
+ - Fixed HTTP-forward agent shutdown to destroy pending proxy sockets and settle requests before the connection handshake completes. (#23) Thanks @SebTardif.
9
+ - Fixed throwing `onEvent` observers to preserve successful installation and leave the proxy runtime stoppable and replaceable. (#23) Thanks @SebTardif.
10
+ - Updated Node types, tsx, ws, and pnpm tooling. (#21, #22)
11
+
3
12
  ## 0.3.7 - 2026-08-02
4
13
 
5
14
  - Fixed Node HTTP/HTTPS proxy agents to apply the default 30-second CONNECT timeout when callers omit a request timeout; explicit `0` remains unbounded. Thanks @SebTardif.
package/dist/env.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,MAAM,WAAW,GACnB,YAAY,GACZ,aAAa,GACb,WAAW,GACX,UAAU,GACV,YAAY,GACZ,aAAa,GACb,WAAW,GACX,UAAU,CAAC;AAEf,KAAK,gBAAgB,GAAG,YAAY,GAAG,aAAa,GAAG,WAAW,GAAG,UAAU,CAAC;AAEhF,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;AAEjF,eAAO,MAAM,eAAe,EAAE,gBAS7B,CAAC;AAEF,wBAAgB,YAAY,IAAI,gBAAgB,CAW/C;AAoBD,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,gBAAgB,EACrB,GAAG,EAAE,gBAAgB,GACpB,MAAM,GAAG,SAAS,CAEpB;AAED,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAElE;AAuHD,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,GAAG,EAAE,gBAAgB,GACpB,MAAM,GAAG,SAAS,CAsBpB;AAED,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,gBAAgB,GAAG,aAAa,CAmC/E"}
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,MAAM,WAAW,GACnB,YAAY,GACZ,aAAa,GACb,WAAW,GACX,UAAU,GACV,YAAY,GACZ,aAAa,GACb,WAAW,GACX,UAAU,CAAC;AAEf,KAAK,gBAAgB,GAAG,YAAY,GAAG,aAAa,GAAG,WAAW,GAAG,UAAU,CAAC;AAEhF,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;AAEjF,eAAO,MAAM,eAAe,EAAE,gBAS7B,CAAC;AAEF,wBAAgB,YAAY,IAAI,gBAAgB,CAW/C;AAoBD,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,gBAAgB,EACrB,GAAG,EAAE,gBAAgB,GACpB,MAAM,GAAG,SAAS,CAEpB;AAED,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAElE;AA4HD,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,GAAG,EAAE,gBAAgB,GACpB,MAAM,GAAG,SAAS,CAsBpB;AAED,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,gBAAgB,GAAG,aAAa,CAmC/E"}
package/dist/env.js CHANGED
@@ -103,7 +103,12 @@ function matchesNoProxy(url, env) {
103
103
  return false;
104
104
  }
105
105
  function normalizeNoProxyHost(hostname) {
106
- const normalized = hostname.trim().toLowerCase().replace(/\.+$/, "");
106
+ const value = hostname.trim().toLowerCase();
107
+ let end = value.length;
108
+ while (end > 0 && value[end - 1] === ".") {
109
+ end -= 1;
110
+ }
111
+ const normalized = value.slice(0, end);
107
112
  return normalized.startsWith("[") && normalized.endsWith("]")
108
113
  ? normalized.slice(1, -1)
109
114
  : normalized;
@@ -9,8 +9,10 @@ export type NodeHttpRequestOptions = http.RequestOptions & https.RequestOptions
9
9
  type NodeHttpMethod = typeof http.request;
10
10
  type NodeAgentFactory = (options: NodeHttpRequestOptions) => http.Agent;
11
11
  type NodeAgentOptions = http.AgentOptions & https.AgentOptions;
12
+ declare const pendingRequestSymbol: unique symbol;
12
13
  type NodeAgentRequestOptions = http.RequestOptions & https.RequestOptions & {
13
14
  secureEndpoint?: boolean;
15
+ [pendingRequestSymbol]?: http.ClientRequest;
14
16
  };
15
17
  type NodeProxyAgentOptions = NodeAgentOptions & {
16
18
  defaultProtocol?: "http" | "https";
@@ -1 +1 @@
1
- {"version":3,"file":"node-http.d.ts","sourceRoot":"","sources":["../src/node-http.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"node-http.d.ts","sourceRoot":"","sources":["../src/node-http.ts"],"names":[],"mappings":"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"}
package/dist/node-http.js CHANGED
@@ -6,6 +6,7 @@ import { domainToASCII } from "node:url";
6
6
  import { readProxyEnv, resolveAmbientProxyForUrl, } from "./env.js";
7
7
  import { formatConnectAuthority, resolveProxyConnectTimeoutMs } from "./connect.js";
8
8
  import { ProxylineError, decodeProxyUserinfoComponent, resolveProxyTlsCa } from "./shared.js";
9
+ const pendingRequestSymbol = Symbol("proxyline.pendingRequest");
9
10
  const MAX_CONNECT_RESPONSE_HEADER_BYTES = 16 * 1024;
10
11
  const INVALID_PROXY_TARGET_HOST_DELIMITER_PATTERN = /[/:?#@\\]/;
11
12
  const INVALID_PROXY_TARGET_HOST_CONTROL_PATTERN = /[\u0000-\u0020\u007f]/;
@@ -295,9 +296,25 @@ function destinationTlsConnectOptions(options, socket) {
295
296
  delete tlsOptions.agent;
296
297
  return tlsOptions;
297
298
  }
298
- class ProxylineHttpForwardAgent extends http.Agent {
299
+ class ProxylineRequestAgent extends http.Agent {
300
+ addRequest(req, options) {
301
+ http.Agent.prototype.addRequest.call(this, req, options);
302
+ }
303
+ createSocket(req, options, callback) {
304
+ // Node chooses queued requests by origin, not global FIFO, and clones options.
305
+ http.Agent.prototype.createSocket.call(this, req, { ...options, [pendingRequestSymbol]: req }, callback);
306
+ }
307
+ takePendingRequest(options) {
308
+ const request = options[pendingRequestSymbol];
309
+ // Node retains these options in pooled socket listeners after handoff.
310
+ delete options[pendingRequestSymbol];
311
+ return request;
312
+ }
313
+ }
314
+ class ProxylineHttpForwardAgent extends ProxylineRequestAgent {
299
315
  options;
300
316
  #keepAlive;
317
+ #pendingConnectSockets = new Set();
301
318
  #proxy;
302
319
  #proxyTls;
303
320
  constructor(proxy, options, proxyTls) {
@@ -310,34 +327,170 @@ class ProxylineHttpForwardAgent extends http.Agent {
310
327
  addRequest(req, options) {
311
328
  setForwardProxyRequestPath(req, options);
312
329
  setProxyRequestHeaders(req, this.#proxy, this.#keepAlive);
313
- http.Agent.prototype.addRequest.call(this, req, options);
330
+ super.addRequest(req, options);
314
331
  }
315
- createConnection(_options, callback) {
332
+ createConnection(options, callback) {
333
+ const request = this.takePendingRequest(options);
316
334
  const socket = connectToProxy(this.#proxy, this.#proxyTls);
317
335
  // When Node supplies an async callback, deliver the socket only through that
318
336
  // path. Returning the same socket as well double-invokes Agent setup and can
319
337
  // hand an unready TLS proxy socket to plain HTTP forward traffic.
320
338
  if (callback !== undefined) {
321
- const onError = (error) => {
339
+ this.#pendingConnectSockets.add(socket);
340
+ let pendingTimeout;
341
+ let settled = false;
342
+ let originalRequestSetTimeout;
343
+ let hookedRequestSetTimeout;
344
+ let originalRequestDestroy;
345
+ let hookedRequestDestroy;
346
+ const restoreRequestDestroyHook = () => {
347
+ if (request !== undefined &&
348
+ originalRequestDestroy !== undefined &&
349
+ request.destroy === hookedRequestDestroy) {
350
+ request.destroy = originalRequestDestroy;
351
+ }
352
+ originalRequestDestroy = undefined;
353
+ hookedRequestDestroy = undefined;
354
+ };
355
+ const startPendingTimeout = (timeoutMs) => {
356
+ if (pendingTimeout !== undefined) {
357
+ clearTimeout(pendingTimeout);
358
+ }
359
+ pendingTimeout = setTimeout(() => {
360
+ request?.emit("timeout");
361
+ if (!settled) {
362
+ fail(new ProxylineError("CONNECT_FAILED", "proxy connection timed out"));
363
+ }
364
+ }, timeoutMs);
365
+ pendingTimeout.unref?.();
366
+ };
367
+ const clearPendingTimeout = () => {
368
+ if (pendingTimeout !== undefined) {
369
+ clearTimeout(pendingTimeout);
370
+ pendingTimeout = undefined;
371
+ }
372
+ };
373
+ const restoreRequestTimeoutHook = () => {
374
+ if (request !== undefined &&
375
+ originalRequestSetTimeout !== undefined &&
376
+ request.setTimeout === hookedRequestSetTimeout) {
377
+ request.setTimeout = originalRequestSetTimeout;
378
+ }
379
+ originalRequestSetTimeout = undefined;
380
+ hookedRequestSetTimeout = undefined;
381
+ };
382
+ const cleanup = () => {
383
+ clearPendingTimeout();
384
+ this.#pendingConnectSockets.delete(socket);
385
+ restoreRequestTimeoutHook();
386
+ restoreRequestDestroyHook();
387
+ socket.off(this.#proxy.protocol === "https:" ? "secureConnect" : "connect", onConnected);
388
+ socket.off("error", onError);
389
+ socket.off("close", onClosed);
390
+ request?.off("abort", onRequestClosed);
391
+ request?.off("close", onRequestClosed);
392
+ request?.off("error", onRequestClosed);
393
+ request?.off("timeout", onRequestTimedOut);
394
+ };
395
+ const finish = (error) => {
396
+ if (settled) {
397
+ return;
398
+ }
399
+ settled = true;
400
+ cleanup();
322
401
  callback(error, socket);
323
402
  };
403
+ const fail = (error) => {
404
+ socket.destroy();
405
+ finish(error);
406
+ };
407
+ const onError = (error) => {
408
+ finish(error);
409
+ };
410
+ const onClosed = () => {
411
+ finish(new ProxylineError("CONNECT_FAILED", "proxy socket closed before connection completed"));
412
+ };
324
413
  const onConnected = () => {
325
- socket.off("error", onError);
326
- callback(null, socket);
414
+ finish(null);
327
415
  };
416
+ const onRequestTimedOut = () => {
417
+ if (!settled) {
418
+ fail(new ProxylineError("CONNECT_FAILED", "proxy connection timed out"));
419
+ }
420
+ };
421
+ const onRequestClosed = () => {
422
+ if (!settled) {
423
+ fail(new ProxylineError("CONNECT_FAILED", "request closed before proxy connection completed"));
424
+ }
425
+ };
426
+ if (request !== undefined) {
427
+ originalRequestSetTimeout = request.setTimeout;
428
+ hookedRequestSetTimeout = function hookedSetTimeout(timeout, callback) {
429
+ const result = originalRequestSetTimeout?.call(this, timeout, callback) ?? this;
430
+ const timeoutMs = normalizedPositiveInteger(timeout);
431
+ if (timeoutMs !== undefined) {
432
+ startPendingTimeout(timeoutMs);
433
+ }
434
+ else {
435
+ clearPendingTimeout();
436
+ }
437
+ return result;
438
+ };
439
+ request.setTimeout = hookedRequestSetTimeout;
440
+ }
441
+ // Prefer explicit agent/request timeout. Default 30s only when both are omitted.
442
+ // Explicit values go through normalizedPositiveInteger first so fractional/invalid
443
+ // timeouts keep the historical no-pending-timer behavior (not Math.trunc to 1ms).
444
+ const requestTimeout = request?.timeout;
445
+ const rawTimeout = options.timeout !== undefined ? options.timeout : requestTimeout;
446
+ if (rawTimeout === undefined) {
447
+ const connectTimeoutMs = resolveProxyConnectTimeoutMs(undefined);
448
+ if (connectTimeoutMs !== undefined) {
449
+ startPendingTimeout(connectTimeoutMs);
450
+ }
451
+ }
452
+ else {
453
+ const timeoutMs = normalizedPositiveInteger(rawTimeout);
454
+ if (timeoutMs !== undefined) {
455
+ startPendingTimeout(timeoutMs);
456
+ }
457
+ }
458
+ request?.once("timeout", onRequestTimedOut);
328
459
  socket.once(this.#proxy.protocol === "https:" ? "secureConnect" : "connect", onConnected);
329
460
  socket.once("error", onError);
461
+ socket.once("close", onClosed);
462
+ // Node defers ClientRequest.destroy() until the async socket callback.
463
+ if (request !== undefined) {
464
+ originalRequestDestroy = request.destroy;
465
+ hookedRequestDestroy = function hookedDestroy(error) {
466
+ const result = originalRequestDestroy?.call(this, error) ?? this;
467
+ onRequestClosed();
468
+ return result;
469
+ };
470
+ request.destroy = hookedRequestDestroy;
471
+ }
472
+ request?.once("abort", onRequestClosed);
473
+ request?.once("close", onRequestClosed);
474
+ request?.once("error", onRequestClosed);
475
+ if (request?.destroyed) {
476
+ onRequestClosed();
477
+ }
330
478
  return undefined;
331
479
  }
332
480
  return socket;
333
481
  }
482
+ destroy() {
483
+ for (const socket of this.#pendingConnectSockets) {
484
+ socket.destroy(new ProxylineError("CONNECT_FAILED", "proxy connection failed because agent was destroyed"));
485
+ }
486
+ this.#pendingConnectSockets.clear();
487
+ super.destroy();
488
+ }
334
489
  }
335
- class ProxylineConnectAgent extends http.Agent {
490
+ class ProxylineConnectAgent extends ProxylineRequestAgent {
336
491
  options;
337
492
  #keepAlive;
338
493
  #pendingConnectSockets = new Set();
339
- #pendingRequests = new WeakMap();
340
- #pendingRequestQueue = [];
341
494
  #proxy;
342
495
  #proxyTls;
343
496
  constructor(proxy, options, proxyTls) {
@@ -347,33 +500,8 @@ class ProxylineConnectAgent extends http.Agent {
347
500
  this.#proxy = proxy;
348
501
  this.#proxyTls = proxyTls;
349
502
  }
350
- addRequest(req, options) {
351
- this.#pendingRequests.set(options, req);
352
- this.#pendingRequestQueue.push(req);
353
- req.once("socket", () => this.#removePendingRequest(req));
354
- req.once("close", () => this.#removePendingRequest(req));
355
- try {
356
- http.Agent.prototype.addRequest.call(this, req, options);
357
- }
358
- catch (error) {
359
- this.#pendingRequests.delete(options);
360
- this.#removePendingRequest(req);
361
- throw error;
362
- }
363
- }
364
- #removePendingRequest(req) {
365
- const index = this.#pendingRequestQueue.indexOf(req);
366
- if (index !== -1) {
367
- this.#pendingRequestQueue.splice(index, 1);
368
- }
369
- }
370
503
  createConnection(options, callback) {
371
- const mappedRequest = this.#pendingRequests.get(options);
372
- const request = mappedRequest ?? this.#pendingRequestQueue.shift();
373
- this.#pendingRequests.delete(options);
374
- if (mappedRequest !== undefined) {
375
- this.#removePendingRequest(mappedRequest);
376
- }
504
+ const request = this.takePendingRequest(options);
377
505
  if (callback === undefined) {
378
506
  throw new ProxylineError("INVALID_CONNECT_CALLBACK", "CONNECT agents require an async socket callback.");
379
507
  }
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAwCA,OAAO,KAAK,EAIV,eAAe,EACf,gBAAgB,EAIjB,MAAM,YAAY,CAAC;AAs0BpB,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,GAAG,eAAe,CAgI3E;AAED,eAAO,MAAM,kBAAkB,yBAAmB,CAAC"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAwCA,OAAO,KAAK,EAIV,eAAe,EACf,gBAAgB,EAIjB,MAAM,YAAY,CAAC;AA00BpB,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,gBAAgB,GAAG,eAAe,CAgI3E;AAED,eAAO,MAAM,kBAAkB,yBAAmB,CAAC"}
package/dist/runtime.js CHANGED
@@ -145,7 +145,12 @@ function normalizeProxyUrl(value) {
145
145
  return url;
146
146
  }
147
147
  function emit(onEvent, event) {
148
- onEvent?.(event);
148
+ try {
149
+ onEvent?.(event);
150
+ }
151
+ catch {
152
+ // Observer failures must not fail install or leave patched globals unstoppable.
153
+ }
149
154
  }
150
155
  function isProxyableUrlProtocol(protocol) {
151
156
  return protocol === "http:" ||
@@ -662,12 +667,6 @@ export function installProxyline(options) {
662
667
  undici: options.undici,
663
668
  })
664
669
  : undefined;
665
- emit(options.onEvent, {
666
- type: "runtime.installed",
667
- mode: options.mode,
668
- active: hasActiveProxy,
669
- ...(redactedProxyUrl ? { proxyUrl: redactedProxyUrl } : {}),
670
- });
671
670
  const handle = {
672
671
  mode: options.mode,
673
672
  active: hasActiveProxy,
@@ -725,6 +724,12 @@ export function installProxyline(options) {
725
724
  },
726
725
  };
727
726
  activeHandle = hasActiveProxy ? handle : activeHandle;
727
+ emit(options.onEvent, {
728
+ type: "runtime.installed",
729
+ mode: options.mode,
730
+ active: hasActiveProxy,
731
+ ...(redactedProxyUrl ? { proxyUrl: redactedProxyUrl } : {}),
732
+ });
728
733
  return handle;
729
734
  }
730
735
  export const installGlobalProxy = installProxyline;
package/docs/surfaces.md CHANGED
@@ -22,6 +22,10 @@ Per request, Proxyline:
22
22
 
23
23
  This means caller agents are not just ignored — the per-request agent is replaced before the original method runs, so even libraries that read `req.agent` after construction see the proxy agent.
24
24
 
25
+ HTTP-forward proxy connections and CONNECT handshakes default to a 30-second connection timeout. A request's `timeout` option or `req.setTimeout(ms)` can change the pending handshake timeout; use `0` to leave it unbounded. Expiry emits `timeout` on that request and fails the pending connection with `CONNECT_FAILED`. This applies to the explicit Node helper agents as well as patched requests.
26
+
27
+ For HTTP-forward requests, `req.destroy()` or `req.abort()` also cancels a pending connection to the proxy. This applies to long-lived helper agents: cancellation releases the pending socket without destroying the whole agent or affecting another queued request.
28
+
25
29
  ### TLS identity preservation
26
30
 
27
31
  When the caller supplied an `https.Agent` with TLS options, the following keys are lifted into the request so the destination TLS handshake still validates correctly:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/proxyline",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "Process-global proxy routing for Node.js.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -17,7 +17,7 @@
17
17
  "url": "https://github.com/openclaw/proxyline/issues"
18
18
  },
19
19
  "homepage": "https://proxyline.dev",
20
- "packageManager": "pnpm@11.20.0",
20
+ "packageManager": "pnpm@11.21.0",
21
21
  "exports": {
22
22
  ".": {
23
23
  "types": "./dist/index.d.ts",
@@ -62,12 +62,12 @@
62
62
  "crabbox:warmup": "crabbox warmup"
63
63
  },
64
64
  "devDependencies": {
65
- "@types/node": "26.1.2",
65
+ "@types/node": "26.2.0",
66
66
  "@types/ws": "8.18.1",
67
- "tsx": "4.23.5",
67
+ "tsx": "4.23.12",
68
68
  "typescript": "^7.0.2",
69
69
  "undici": "8.10.0",
70
- "ws": "8.21.1"
70
+ "ws": "8.21.3"
71
71
  },
72
72
  "engines": {
73
73
  "node": ">=22.19.0"
package/src/env.ts CHANGED
@@ -134,7 +134,12 @@ function matchesNoProxy(url: URL, env: ProxyEnvSnapshot): boolean {
134
134
  }
135
135
 
136
136
  function normalizeNoProxyHost(hostname: string): string {
137
- const normalized = hostname.trim().toLowerCase().replace(/\.+$/, "");
137
+ const value = hostname.trim().toLowerCase();
138
+ let end = value.length;
139
+ while (end > 0 && value[end - 1] === ".") {
140
+ end -= 1;
141
+ }
142
+ const normalized = value.slice(0, end);
138
143
  return normalized.startsWith("[") && normalized.endsWith("]")
139
144
  ? normalized.slice(1, -1)
140
145
  : normalized;
package/src/node-http.ts CHANGED
@@ -19,17 +19,27 @@ export type NodeHttpRequestOptions = http.RequestOptions & https.RequestOptions
19
19
  type NodeHttpMethod = typeof http.request;
20
20
  type NodeAgentFactory = (options: NodeHttpRequestOptions) => http.Agent;
21
21
  type NodeAgentOptions = http.AgentOptions & https.AgentOptions;
22
+ const pendingRequestSymbol = Symbol("proxyline.pendingRequest");
22
23
  type NodeAgentRequestOptions = http.RequestOptions & https.RequestOptions & {
23
24
  secureEndpoint?: boolean;
25
+ [pendingRequestSymbol]?: http.ClientRequest;
24
26
  };
25
27
  type NodeAddRequestAgent = http.Agent & {
26
28
  addRequest(req: http.ClientRequest, options: NodeAgentRequestOptions): void;
27
29
  };
30
+ type NodeCreateSocketAgent = http.Agent & {
31
+ createSocket(
32
+ req: http.ClientRequest,
33
+ options: NodeAgentRequestOptions,
34
+ callback: (error: Error | null, socket?: net.Socket) => void,
35
+ ): void;
36
+ };
28
37
  type RequestSetTimeout = (
29
38
  this: http.ClientRequest,
30
39
  timeout: number,
31
40
  callback?: () => void,
32
41
  ) => http.ClientRequest;
42
+ type RequestDestroy = (this: http.ClientRequest, error?: Error) => http.ClientRequest;
33
43
  type NodeAgentWithOptions = http.Agent & {
34
44
  options?: NodeAgentOptions;
35
45
  };
@@ -417,9 +427,34 @@ function destinationTlsConnectOptions(
417
427
  return tlsOptions;
418
428
  }
419
429
 
420
- class ProxylineHttpForwardAgent extends http.Agent {
430
+ abstract class ProxylineRequestAgent extends http.Agent {
431
+ public addRequest(req: http.ClientRequest, options: NodeAgentRequestOptions): void {
432
+ (http.Agent.prototype as unknown as NodeAddRequestAgent).addRequest.call(this, req, options);
433
+ }
434
+
435
+ public createSocket(
436
+ req: http.ClientRequest,
437
+ options: NodeAgentRequestOptions,
438
+ callback: (error: Error | null, socket?: net.Socket) => void,
439
+ ): void {
440
+ // Node chooses queued requests by origin, not global FIFO, and clones options.
441
+ (http.Agent.prototype as unknown as NodeCreateSocketAgent).createSocket.call(
442
+ this, req, { ...options, [pendingRequestSymbol]: req }, callback,
443
+ );
444
+ }
445
+
446
+ protected takePendingRequest(options: NodeAgentRequestOptions): http.ClientRequest | undefined {
447
+ const request = options[pendingRequestSymbol];
448
+ // Node retains these options in pooled socket listeners after handoff.
449
+ delete options[pendingRequestSymbol];
450
+ return request;
451
+ }
452
+ }
453
+
454
+ class ProxylineHttpForwardAgent extends ProxylineRequestAgent {
421
455
  public readonly options: NodeAgentOptions;
422
456
  readonly #keepAlive: boolean;
457
+ readonly #pendingConnectSockets = new Set<net.Socket>();
423
458
  readonly #proxy: URL;
424
459
  readonly #proxyTls: ProxylineTlsOptions | undefined;
425
460
 
@@ -431,42 +466,189 @@ class ProxylineHttpForwardAgent extends http.Agent {
431
466
  this.#proxyTls = proxyTls;
432
467
  }
433
468
 
434
- public addRequest(req: http.ClientRequest, options: NodeAgentRequestOptions): void {
469
+ public override addRequest(req: http.ClientRequest, options: NodeAgentRequestOptions): void {
435
470
  setForwardProxyRequestPath(req as http.ClientRequest & { _header?: string | null }, options);
436
471
  setProxyRequestHeaders(req, this.#proxy, this.#keepAlive);
437
- (http.Agent.prototype as unknown as NodeAddRequestAgent).addRequest.call(this, req, options);
472
+ super.addRequest(req, options);
438
473
  }
439
474
 
440
475
  public override createConnection(
441
- _options: NodeAgentRequestOptions,
476
+ options: NodeAgentRequestOptions,
442
477
  callback?: (error: Error | null, socket: net.Socket) => void,
443
478
  ): net.Socket {
479
+ const request = this.takePendingRequest(options);
444
480
  const socket = connectToProxy(this.#proxy, this.#proxyTls);
445
481
  // When Node supplies an async callback, deliver the socket only through that
446
482
  // path. Returning the same socket as well double-invokes Agent setup and can
447
483
  // hand an unready TLS proxy socket to plain HTTP forward traffic.
448
484
  if (callback !== undefined) {
449
- const onError = (error: Error): void => {
485
+ this.#pendingConnectSockets.add(socket);
486
+ let pendingTimeout: ReturnType<typeof setTimeout> | undefined;
487
+ let settled = false;
488
+ let originalRequestSetTimeout: RequestSetTimeout | undefined;
489
+ let hookedRequestSetTimeout: RequestSetTimeout | undefined;
490
+ let originalRequestDestroy: RequestDestroy | undefined;
491
+ let hookedRequestDestroy: RequestDestroy | undefined;
492
+
493
+ const restoreRequestDestroyHook = (): void => {
494
+ if (
495
+ request !== undefined &&
496
+ originalRequestDestroy !== undefined &&
497
+ request.destroy === hookedRequestDestroy
498
+ ) {
499
+ request.destroy = originalRequestDestroy;
500
+ }
501
+ originalRequestDestroy = undefined;
502
+ hookedRequestDestroy = undefined;
503
+ };
504
+
505
+ const startPendingTimeout = (timeoutMs: number): void => {
506
+ if (pendingTimeout !== undefined) {
507
+ clearTimeout(pendingTimeout);
508
+ }
509
+ pendingTimeout = setTimeout(() => {
510
+ request?.emit("timeout");
511
+ if (!settled) {
512
+ fail(new ProxylineError("CONNECT_FAILED", "proxy connection timed out"));
513
+ }
514
+ }, timeoutMs);
515
+ pendingTimeout.unref?.();
516
+ };
517
+
518
+ const clearPendingTimeout = (): void => {
519
+ if (pendingTimeout !== undefined) {
520
+ clearTimeout(pendingTimeout);
521
+ pendingTimeout = undefined;
522
+ }
523
+ };
524
+
525
+ const restoreRequestTimeoutHook = (): void => {
526
+ if (
527
+ request !== undefined &&
528
+ originalRequestSetTimeout !== undefined &&
529
+ request.setTimeout === hookedRequestSetTimeout
530
+ ) {
531
+ request.setTimeout = originalRequestSetTimeout;
532
+ }
533
+ originalRequestSetTimeout = undefined;
534
+ hookedRequestSetTimeout = undefined;
535
+ };
536
+
537
+ const cleanup = (): void => {
538
+ clearPendingTimeout();
539
+ this.#pendingConnectSockets.delete(socket);
540
+ restoreRequestTimeoutHook();
541
+ restoreRequestDestroyHook();
542
+ socket.off(this.#proxy.protocol === "https:" ? "secureConnect" : "connect", onConnected);
543
+ socket.off("error", onError);
544
+ socket.off("close", onClosed);
545
+ request?.off("abort", onRequestClosed);
546
+ request?.off("close", onRequestClosed);
547
+ request?.off("error", onRequestClosed);
548
+ request?.off("timeout", onRequestTimedOut);
549
+ };
550
+ const finish = (error: Error | null): void => {
551
+ if (settled) {
552
+ return;
553
+ }
554
+ settled = true;
555
+ cleanup();
450
556
  callback(error, socket);
451
557
  };
558
+ const fail = (error: Error): void => {
559
+ socket.destroy();
560
+ finish(error);
561
+ };
562
+ const onError = (error: Error): void => {
563
+ finish(error);
564
+ };
565
+ const onClosed = (): void => {
566
+ finish(new ProxylineError("CONNECT_FAILED", "proxy socket closed before connection completed"));
567
+ };
452
568
  const onConnected = (): void => {
453
- socket.off("error", onError);
454
- callback(null, socket);
569
+ finish(null);
570
+ };
571
+ const onRequestTimedOut = (): void => {
572
+ if (!settled) {
573
+ fail(new ProxylineError("CONNECT_FAILED", "proxy connection timed out"));
574
+ }
455
575
  };
576
+ const onRequestClosed = (): void => {
577
+ if (!settled) {
578
+ fail(new ProxylineError("CONNECT_FAILED", "request closed before proxy connection completed"));
579
+ }
580
+ };
581
+
582
+ if (request !== undefined) {
583
+ originalRequestSetTimeout = request.setTimeout;
584
+ hookedRequestSetTimeout = function hookedSetTimeout(timeout, callback) {
585
+ const result = originalRequestSetTimeout?.call(this, timeout, callback) ?? this;
586
+ const timeoutMs = normalizedPositiveInteger(timeout);
587
+ if (timeoutMs !== undefined) {
588
+ startPendingTimeout(timeoutMs);
589
+ } else {
590
+ clearPendingTimeout();
591
+ }
592
+ return result;
593
+ };
594
+ request.setTimeout = hookedRequestSetTimeout;
595
+ }
596
+
597
+ // Prefer explicit agent/request timeout. Default 30s only when both are omitted.
598
+ // Explicit values go through normalizedPositiveInteger first so fractional/invalid
599
+ // timeouts keep the historical no-pending-timer behavior (not Math.trunc to 1ms).
600
+ const requestTimeout = (request as { timeout?: unknown } | undefined)?.timeout;
601
+ const rawTimeout = options.timeout !== undefined ? options.timeout : requestTimeout;
602
+ if (rawTimeout === undefined) {
603
+ const connectTimeoutMs = resolveProxyConnectTimeoutMs(undefined);
604
+ if (connectTimeoutMs !== undefined) {
605
+ startPendingTimeout(connectTimeoutMs);
606
+ }
607
+ } else {
608
+ const timeoutMs = normalizedPositiveInteger(rawTimeout);
609
+ if (timeoutMs !== undefined) {
610
+ startPendingTimeout(timeoutMs);
611
+ }
612
+ }
613
+ request?.once("timeout", onRequestTimedOut);
614
+
456
615
  socket.once(this.#proxy.protocol === "https:" ? "secureConnect" : "connect", onConnected);
457
616
  socket.once("error", onError);
617
+ socket.once("close", onClosed);
618
+ // Node defers ClientRequest.destroy() until the async socket callback.
619
+ if (request !== undefined) {
620
+ originalRequestDestroy = request.destroy;
621
+ hookedRequestDestroy = function hookedDestroy(this: http.ClientRequest, error?: Error) {
622
+ const result = originalRequestDestroy?.call(this, error) ?? this;
623
+ onRequestClosed();
624
+ return result;
625
+ };
626
+ request.destroy = hookedRequestDestroy;
627
+ }
628
+ request?.once("abort", onRequestClosed);
629
+ request?.once("close", onRequestClosed);
630
+ request?.once("error", onRequestClosed);
631
+ if (request?.destroyed) {
632
+ onRequestClosed();
633
+ }
458
634
  return undefined as unknown as net.Socket;
459
635
  }
460
636
  return socket;
461
637
  }
638
+
639
+ public override destroy(): void {
640
+ for (const socket of this.#pendingConnectSockets) {
641
+ socket.destroy(new ProxylineError("CONNECT_FAILED", "proxy connection failed because agent was destroyed"));
642
+ }
643
+ this.#pendingConnectSockets.clear();
644
+ super.destroy();
645
+ }
462
646
  }
463
647
 
464
- class ProxylineConnectAgent extends http.Agent {
648
+ class ProxylineConnectAgent extends ProxylineRequestAgent {
465
649
  public readonly options: NodeAgentOptions;
466
650
  readonly #keepAlive: boolean;
467
651
  readonly #pendingConnectSockets = new Set<net.Socket>();
468
- readonly #pendingRequests = new WeakMap<NodeAgentRequestOptions, http.ClientRequest>();
469
- readonly #pendingRequestQueue: http.ClientRequest[] = [];
470
652
  readonly #proxy: URL;
471
653
  readonly #proxyTls: ProxylineTlsOptions | undefined;
472
654
 
@@ -478,37 +660,11 @@ class ProxylineConnectAgent extends http.Agent {
478
660
  this.#proxyTls = proxyTls;
479
661
  }
480
662
 
481
- public addRequest(req: http.ClientRequest, options: NodeAgentRequestOptions): void {
482
- this.#pendingRequests.set(options, req);
483
- this.#pendingRequestQueue.push(req);
484
- req.once("socket", () => this.#removePendingRequest(req));
485
- req.once("close", () => this.#removePendingRequest(req));
486
- try {
487
- (http.Agent.prototype as unknown as NodeAddRequestAgent).addRequest.call(this, req, options);
488
- } catch (error) {
489
- this.#pendingRequests.delete(options);
490
- this.#removePendingRequest(req);
491
- throw error;
492
- }
493
- }
494
-
495
- #removePendingRequest(req: http.ClientRequest): void {
496
- const index = this.#pendingRequestQueue.indexOf(req);
497
- if (index !== -1) {
498
- this.#pendingRequestQueue.splice(index, 1);
499
- }
500
- }
501
-
502
663
  public override createConnection(
503
664
  options: NodeAgentRequestOptions,
504
665
  callback?: (error: Error | null, socket: net.Socket) => void,
505
666
  ): net.Socket {
506
- const mappedRequest = this.#pendingRequests.get(options);
507
- const request = mappedRequest ?? this.#pendingRequestQueue.shift();
508
- this.#pendingRequests.delete(options);
509
- if (mappedRequest !== undefined) {
510
- this.#removePendingRequest(mappedRequest);
511
- }
667
+ const request = this.takePendingRequest(options);
512
668
  if (callback === undefined) {
513
669
  throw new ProxylineError("INVALID_CONNECT_CALLBACK", "CONNECT agents require an async socket callback.");
514
670
  }
package/src/runtime.ts CHANGED
@@ -283,7 +283,11 @@ function normalizeProxyUrl(value: string | URL | undefined): URL | undefined {
283
283
  }
284
284
 
285
285
  function emit(onEvent: ProxylineOptions["onEvent"], event: ProxylineEvent): void {
286
- onEvent?.(event);
286
+ try {
287
+ onEvent?.(event);
288
+ } catch {
289
+ // Observer failures must not fail install or leave patched globals unstoppable.
290
+ }
287
291
  }
288
292
 
289
293
  function isProxyableUrlProtocol(protocol: string): boolean {
@@ -942,12 +946,6 @@ export function installProxyline(options: ProxylineOptions): ProxylineHandle {
942
946
  },
943
947
  )
944
948
  : undefined;
945
- emit(options.onEvent, {
946
- type: "runtime.installed",
947
- mode: options.mode,
948
- active: hasActiveProxy,
949
- ...(redactedProxyUrl ? { proxyUrl: redactedProxyUrl } : {}),
950
- });
951
949
 
952
950
  const handle: ProxylineHandle = {
953
951
  mode: options.mode,
@@ -1012,6 +1010,12 @@ export function installProxyline(options: ProxylineOptions): ProxylineHandle {
1012
1010
  };
1013
1011
 
1014
1012
  activeHandle = hasActiveProxy ? handle : activeHandle;
1013
+ emit(options.onEvent, {
1014
+ type: "runtime.installed",
1015
+ mode: options.mode,
1016
+ active: hasActiveProxy,
1017
+ ...(redactedProxyUrl ? { proxyUrl: redactedProxyUrl } : {}),
1018
+ });
1015
1019
  return handle;
1016
1020
  }
1017
1021