From 99f1540d8c51e843ccbd767ce4f3dd32270d35c5 Mon Sep 17 00:00:00 2001 From: Aleksei Sviridkin <3811295@gmail.com> Date: Tue, 3 Feb 2026 08:16:46 +0300 Subject: [PATCH] Enhancement: DNS fallback for Alpine/musl compatibility (#6265) Signed-off-by: Aleksei Sviridkin Co-authored-by: Claude Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- src/utils/proxy/http.js | 123 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 122 insertions(+), 1 deletion(-) diff --git a/src/utils/proxy/http.js b/src/utils/proxy/http.js index d61cc5c7f..c05102142 100644 --- a/src/utils/proxy/http.js +++ b/src/utils/proxy/http.js @@ -1,3 +1,5 @@ +import dns from "node:dns"; +import net from "node:net"; import { createUnzip, constants as zlibConstants } from "node:zlib"; import { http, https } from "follow-redirects"; @@ -106,10 +108,129 @@ export async function cachedRequest(url, duration = 5, ua = "homepage") { return data; } +// Custom DNS lookup that falls back to Node.js c-ares resolver (dns.resolve) +// when system getaddrinfo (dns.lookup) fails with ENOTFOUND/EAI_NONAME. +// Fixes DNS resolution issues with Alpine/musl libc in k8s +const FALLBACK_CODES = new Set(["ENOTFOUND", "EAI_NONAME"]); + +function homepageDNSLookupFn() { + const normalizeOptions = (options) => { + if (typeof options === "number") { + return { family: options, all: false, lookupOptions: { family: options } }; + } + + const normalized = options ?? {}; + return { + family: normalized.family, + all: Boolean(normalized.all), + lookupOptions: normalized, + }; + }; + + return (hostname, options, callback) => { + // Handle case where options is the callback (2-argument form) + if (typeof options === "function") { + callback = options; + options = {}; + } + + const { family, all, lookupOptions } = normalizeOptions(options); + const sendResponse = (addr, fam) => { + if (all) { + let addresses = addr; + if (!Array.isArray(addresses)) { + addresses = [{ address: addresses, family: fam }]; + } else if (addresses.length && typeof addresses[0] === "string") { + addresses = addresses.map((a) => ({ address: a, family: fam })); + } + + callback(null, addresses); + } else { + callback(null, addr, fam); + } + }; + + // If hostname is already an IP address, return it directly + const ipVersion = net.isIP(hostname); + if (ipVersion) { + sendResponse(hostname, ipVersion); + return; + } + + // Try dns.lookup first (preserves /etc/hosts behavior) + dns.lookup(hostname, lookupOptions, (lookupErr, address, lookupFamily) => { + if (!lookupErr) { + sendResponse(address, lookupFamily); + return; + } + + // ENOTFOUND or EAI_NONAME will try fallback, otherwise return error here + if (!FALLBACK_CODES.has(lookupErr.code)) { + callback(lookupErr); + return; + } + + const finalize = (addresses, resolvedFamily) => { + // Finalize the resolution and call the callback + if (!addresses || addresses.length === 0) { + const err = new Error(`No addresses found for hostname: ${hostname}`); + err.code = "ENOTFOUND"; + callback(err); + return; + } + + logger.debug("DNS fallback to c-ares resolver succeeded for %s", hostname); + + sendResponse(addresses, resolvedFamily); + }; + + const resolveOnce = (fn, resolvedFamily, onFail) => { + // attempt resolution with a specific resolver + fn(hostname, (err, addresses) => { + if (!err) { + finalize(addresses, resolvedFamily); + return; + } + onFail(err); + }); + }; + + const handleFallbackFailure = (resolveErr) => { + // handle final fallback failure with full context + logger.debug( + "DNS fallback failed for %s: lookup error=%s, resolve error=%s", + hostname, + lookupErr.code, + resolveErr?.code, + ); + callback(resolveErr || lookupErr); + }; + + // Fallback to c-ares (dns.resolve*). If family isn't specified, try v4 then v6. + if (family === 6) { + resolveOnce(dns.resolve6, 6, handleFallbackFailure); + return; + } + + if (family === 4) { + resolveOnce(dns.resolve4, 4, handleFallbackFailure); + return; + } + + resolveOnce(dns.resolve4, 4, () => { + resolveOnce(dns.resolve6, 6, handleFallbackFailure); + }); + }); + }; +} + export async function httpProxy(url, params = {}) { const constructedUrl = new URL(url); const disableIpv6 = process.env.HOMEPAGE_PROXY_DISABLE_IPV6 === "true"; - const agentOptions = disableIpv6 ? { family: 4, autoSelectFamily: false } : { autoSelectFamilyAttemptTimeout: 500 }; + const agentOptions = { + ...(disableIpv6 ? { family: 4, autoSelectFamily: false } : { autoSelectFamilyAttemptTimeout: 500 }), + lookup: homepageDNSLookupFn(), + }; let request = null; if (constructedUrl.protocol === "https:") {