commit a04e88fc2021cd00e52e2f76c216764374d6a3cc
parent 8f6fb55041d02b0dfac41f4e3cc08a0485ed23f2
Author: Christian Grothoff <christian@grothoff.org>
Date: Sat, 31 Oct 2026 13:14:25 +0100
improve fix for #9942: go back to happy-eye-balls, but with increased timeout
Diffstat:
1 file changed, 36 insertions(+), 15 deletions(-)
diff --git a/packages/taler-util/src/http-impl.node.ts b/packages/taler-util/src/http-impl.node.ts
@@ -20,7 +20,7 @@ import type { FollowOptions, RedirectableRequest } from "follow-redirects";
import followRedirects from "follow-redirects";
import type { ClientRequest, IncomingMessage } from "node:http";
import { RequestOptions } from "node:http";
-import * as net from "node:net";
+import type * as net from "node:net";
import { deflateSync, gzipSync } from "node:zlib";
import {
HeadersImpl,
@@ -37,19 +37,33 @@ import { URL } from "./url.js";
const http = followRedirects.http;
const https = followRedirects.https;
-// Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed
-// in v20.3.0.
-// https://github.com/nodejs/node/issues/47822#issuecomment-1564708870
-// The issues is not fixed on 22 and 24.
-// More info https://bugs.gnunet.org/view.php?id=9942
-if (
- // check for `node` in case we want to use this in "exotic" JS envs
- process.versions.node &&
- process.versions.node.match(/(20|22|24)\./)
-) {
- //@ts-ignore
- net.setDefaultAutoSelectFamily(false);
-}
+// Node races the IPv6 and IPv4 addresses of a host against each other
+// ("happy eyeballs", on by default since v20) and keeps whichever connects
+// first. v20.0.0 to v20.2.0 got the race wrong and tore down both
+// connections, so requests failed with ERR_SOCKET_CONNECTION_TIMEOUT. That
+// was fixed in v20.3.0 by nodejs/node#47860, see
+// https://github.com/nodejs/node/issues/47822#issuecomment-1564708870 and
+// https://bugs.gnunet.org/view.php?id=9942
+//
+// Turn the race off on those three releases only, and per request rather than
+// process-wide with net.setDefaultAutoSelectFamily(): this is a library, so a
+// global would also reconfigure every other HTTP client in the embedding
+// process. Do not widen the range either -- without the race node connects to
+// whatever address getaddrinfo returns first and never tries the remaining
+// ones, so on a dual-stack host, where "localhost" resolves to ::1 first, any
+// service listening on IPv4 only becomes unreachable with no second attempt to
+// fall back to.
+const autoSelectFamily: boolean | undefined =
+ process.versions.node && /^20\.[0-2]\./.test(process.versions.node)
+ ? false
+ : undefined;
+
+// What the later reports in that node issue describe is the per-attempt
+// budget: node gives the first address 250ms to connect before it starts the
+// attempt on the next one, which is tight enough on a slow mobile link to turn
+// a merely slow address family into a failed request. Be more patient instead
+// of giving up the fallback.
+const autoSelectFamilyAttemptTimeout = 1000;
const logger = new Logger("http-impl.node.ts");
@@ -111,7 +125,12 @@ export const rawLib: HttpRawLib = {
path += parsedUrl.search;
}
- const options: RequestOptions & FollowOptions<RequestOptions> = {
+ const options: RequestOptions &
+ FollowOptions<RequestOptions> &
+ Pick<
+ net.TcpSocketConnectOpts,
+ "autoSelectFamily" | "autoSelectFamilyAttemptTimeout"
+ > = {
protocol: parsedUrl.protocol,
port: parsedUrl.port,
host: parsedUrl.hostname,
@@ -119,6 +138,8 @@ export const rawLib: HttpRawLib = {
path,
headers,
timeout: opt.timeoutMs,
+ autoSelectFamily,
+ autoSelectFamilyAttemptTimeout,
followRedirects: opt.redirect !== "manual",
beforeRedirect:
opt.redirect === "error"