Skip to content
Source
Sign inSign up
Open History to see the last commit

outboundHttp.ts

68 lines3.0 KB

1import * as NodeModule from "node:module";
2import * as NodeNet from "node:net";
3​
4import type * as Undici from "undici";
5​
6// undici is external to the CLI bundle; Node single-executables can load it from disk only with require.
7const requireUndici = NodeModule.createRequire(import.meta.url);
8​
9/** Node 26 bundles h2-capable undici 8; Node 22/24 and Electron stay on HTTP/1.1 with 6/7. */
10export const builtinFetchUsesHttp2 = (bundledUndiciVersion: string | undefined) =>
11 Number.parseInt(bundledUndiciVersion ?? "", 10) >= 8;
12​
13/** Preserves the EnvHttpProxyAgent installed by --use-env-proxy / NODE_USE_ENV_PROXY. */
14export function installHttp1FetchDispatcher(): void {
15 const undici = requireUndici("undici") as typeof Undici;
16 // Node's agent comes from its internal undici copy, so `instanceof` against the npm class is false.
17 const usesEnvProxy = undici.getGlobalDispatcher().constructor.name === "EnvHttpProxyAgent";
18 undici.setGlobalDispatcher(
19 usesEnvProxy
20 ? new undici.EnvHttpProxyAgent({
21 allowH2: false,
22 // ProxyAgent's connector forces h2 and ignores allowH2; replace it for HTTPS proxies.
23 clientFactory: (origin) => new undici.Pool(origin, { allowH2: false }),
24 })
25 : new undici.Agent({ allowH2: false }),
26 );
28​
29/** Avoids uncaught idle-timeout errors on h2 streams (https://github.com/nodejs/undici/issues/5936). */
30export function keepOutboundFetchOnHttp1(): void {
31 if (builtinFetchUsesHttp2(process.versions.undici)) installHttp1FetchDispatcher();
33​
34// @types/node does not declare setTypeOfService yet.
35type TypeOfServiceSocket = {
36 setTypeOfService?: (this: NodeNet.Socket, typeOfService: number) => NodeNet.Socket;
37};
38​
39// Bundled undici calls this from a socket callback without catching. On macOS,
40// EINVAL after a peer reset must not exit the server over an optional QoS hint.
41export function ignoreMacosTypeOfServiceEinval(
42 // oxlint-disable-next-line t3code/no-global-process-runtime -- Runs before ACP dispatch without loading the Effect graph.
43 platform = process.platform,
44 socketPrototype: TypeOfServiceSocket = NodeNet.Socket.prototype as TypeOfServiceSocket,
45): void {
46 if (platform !== "darwin") return;
47 const setTypeOfService = socketPrototype.setTypeOfService;
48 if (typeof setTypeOfService !== "function") return;
49 socketPrototype.setTypeOfService = function (...args) {
50 try {
51 return setTypeOfService.apply(this, args);
52 } catch (error) {
53 if (error instanceof Error && "code" in error && error.code === "EINVAL") return this;
54 throw error;
55 }
56 };
58​
59/**
60 * Node's 250 ms Happy Eyeballs attempt timeout abandons a working-but-slow IPv4
61 * connect when IPv6 is advertised but unroutable, which broke the ACP registry
62 * fetch. A larger `--network-family-autoselection-attempt-timeout` still wins.
63 */
64export function extendConnectionAttemptTimeout(): void {
65 NodeNet.setDefaultAutoSelectFamilyAttemptTimeout(
66 Math.max(NodeNet.getDefaultAutoSelectFamilyAttemptTimeout(), 1_000),
67 );