taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

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:
Mpackages/taler-util/src/http-impl.node.ts | 51++++++++++++++++++++++++++++++++++++---------------
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"