From 4d39385b91ec91e07a8611522e513f5b4ca12517 Mon Sep 17 00:00:00 2001 From: Nazar Leush Date: Wed, 7 Oct 2026 17:09:21 +0300 Subject: [PATCH 01/18] support `options.prefer_ipv6` and use `ipv6first` for it --- lib/fetch.js | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/lib/fetch.js b/lib/fetch.js index b7f29f8dd..0d368689c 100644 --- a/lib/fetch.js +++ b/lib/fetch.js @@ -1,4 +1,5 @@ import { URL } from 'url'; +import dns from 'node:dns'; import { h1NoCache, noCache, createUrl, AbortController, AbortError, FetchError } from '@adobe/fetch'; import { serialize as serializeCookie, parse as parseCookie } from 'cookie'; import log from '../logging.js'; @@ -38,6 +39,28 @@ const fetchH1 = h1NoCache({ rejectUnauthorized: false // By default skip auth check for all. }).fetch; +// Sorts AAAA first for this context only; autoSelectFamily falls back to IPv4 on connect failure. Requires Node >= 20.13. +const lookupIPv6First = (hostname, options, callback) => + dns.lookup(hostname, { ...options, order: 'ipv6first' }, callback); + +const fetchIPv6First = noCache({ + h1: { + lookup: lookupIPv6First + }, + h2: { + enablePush: false, + lookup: lookupIPv6First + }, + rejectUnauthorized: false // By default skip auth check for all. +}).fetch; + +const fetchH1IPv6First = h1NoCache({ + h1: { + lookup: lookupIPv6First + }, + rejectUnauthorized: false // By default skip auth check for all. +}).fetch; + export function skipLoggingFetchError(error) { return error?.name === 'AbortError' || error?.code === 'ECONNRESET' @@ -49,6 +72,13 @@ export function skipLoggingFetchError(error) { || error?.code === 'ETIMEDOUT'; } +function selectFetchFunc(options, fetch_func = fetch, h1_fetch_func = fetchH1) { + if (options.prefer_ipv6) { + return options.disable_http2 ? fetchH1IPv6First : fetchIPv6First; + } + return options.disable_http2 ? h1_fetch_func : fetch_func; +} + function doFetch(fetch_func, h1_fetch_func, options) { const fetch_options = Object.assign({}, options); @@ -73,7 +103,7 @@ function doFetch(fetch_func, h1_fetch_func, options) { abortController.abort(); }, options.timeout || CONFIG.RESPONSE_TIMEOUT); - const a_fetch_func = options.disable_http2 ? h1_fetch_func: fetch_func; + const a_fetch_func = selectFetchFunc(options, fetch_func, h1_fetch_func); return new Promise((resolve, reject) => { a_fetch_func(uri, fetch_options) .then(response => { @@ -184,7 +214,7 @@ export function fetchData(options) { abortController.abort(); }, options.timeout || CONFIG.RESPONSE_TIMEOUT); - const a_fetch_func = options.disable_http2 ? fetchH1: fetch; + const a_fetch_func = selectFetchFunc(options); return new Promise((resolve, reject) => { a_fetch_func(uri, fetch_options) .then(response => { From e0e22300c001b8b88858167b83f9af69f94f4c42 Mon Sep 17 00:00:00 2001 From: Nazar Leush Date: Wed, 7 Oct 2026 17:11:36 +0300 Subject: [PATCH 02/18] read `prefer_ipv6` from proxy config --- lib/utils.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/utils.js b/lib/utils.js index 6a80e9ade..071dc8914 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -153,6 +153,9 @@ export function prepareRequestOptions(request_options, options) { if (proxy.disable_http2) { request_options.disable_http2 = true; } + if (proxy.prefer_ipv6) { + request_options.prefer_ipv6 = true; + } if (proxy.disable_language) { disable_language = true; } From 20a42897f3b94f973b72dd2225d961c44befa94a Mon Sep 17 00:00:00 2001 From: Nazar Leush Date: Wed, 7 Oct 2026 17:13:31 +0300 Subject: [PATCH 03/18] debug ipv6 lookup --- lib/fetch.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/fetch.js b/lib/fetch.js index 0d368689c..0ed254165 100644 --- a/lib/fetch.js +++ b/lib/fetch.js @@ -41,7 +41,17 @@ const fetchH1 = h1NoCache({ // Sorts AAAA first for this context only; autoSelectFamily falls back to IPv4 on connect failure. Requires Node >= 20.13. const lookupIPv6First = (hostname, options, callback) => - dns.lookup(hostname, { ...options, order: 'ipv6first' }, callback); + dns.lookup(hostname, { ...options, order: 'ipv6first' }, (error, address, family) => { + if (error) { + log(' -- lookupIPv6First error', hostname, error.code, error.message); + } else if (Array.isArray(address)) { + // With autoSelectFamily lookup is called with `all: true`. + log(' -- lookupIPv6First', hostname, address.map(a => `${a.address} (v${a.family})`).join(', ')); + } else { + log(' -- lookupIPv6First', hostname, `${address} (v${family})`); + } + callback(error, address, family); + }); const fetchIPv6First = noCache({ h1: { From e2593b32909d9d3fe9d24dfc2c9b5e0b79b1ca39 Mon Sep 17 00:00:00 2001 From: Nazar Leush Date: Wed, 7 Oct 2026 17:20:31 +0300 Subject: [PATCH 04/18] comment --- lib/fetch.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/fetch.js b/lib/fetch.js index 0ed254165..3c09cf36d 100644 --- a/lib/fetch.js +++ b/lib/fetch.js @@ -189,6 +189,7 @@ function doFetch(fetch_func, h1_fetch_func, options) { } export function fetchStreamKeepAlive(options) { + // TODO: ipv6 priority? return doFetch(fetchKeepAlive, fetchH1KeepAlive, options); } @@ -197,6 +198,7 @@ export function fetchStream(options) { }; export function fetchStreamAuthorized(options) { + // TODO: ipv6 priority? return doFetch(fetchAuthorized, fetchH1Authorized, options); }; From cff484abb3cec3e9c324829163c480cd92669c8d Mon Sep 17 00:00:00 2001 From: Nazar Leush Date: Wed, 7 Oct 2026 18:11:56 +0300 Subject: [PATCH 05/18] patch `@adobe/fetch` to support custom agent dns lookup --- patches/@adobe__fetch.patch | 21 +++++++++++++++++++++ pnpm-lock.yaml | 5 +++-- pnpm-workspace.yaml | 1 + 3 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 patches/@adobe__fetch.patch diff --git a/patches/@adobe__fetch.patch b/patches/@adobe__fetch.patch new file mode 100644 index 000000000..a57987b1c --- /dev/null +++ b/patches/@adobe__fetch.patch @@ -0,0 +1,21 @@ +diff --git a/src/core/request.js b/src/core/request.js +index 6f7120e3e383ed44b4fe63861109f365579d7425..19de7a26bc712e172b78efb4de4ef97ad8f269e3 100644 +--- a/src/core/request.js ++++ b/src/core/request.js +@@ -160,7 +160,16 @@ const determineProtocol = async (ctx, url, signal) => { + const rejectUnauthorized = !((_rejectUnauthorized === false + || h1Opts.rejectUnauthorized === false + || h2Opts.rejectUnauthorized === false)); ++ // PATCH: forward DNS/socket options from context to the ALPN negotiation socket ++ const socketOptions = {}; ++ ['lookup', 'family', 'autoSelectFamily', 'autoSelectFamilyAttemptTimeout'].forEach((name) => { ++ const value = h2Opts[name] !== undefined ? h2Opts[name] : h1Opts[name]; ++ if (value !== undefined) { ++ socketOptions[name] = value; ++ } ++ }); + const connectOptions = { ++ ...socketOptions, + servername: url.hostname, // enable SNI (Server Name Indication) extension + ALPNProtocols: ctx.alpnProtocols, + signal, // optional abort signal diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5acafc28a..d3dce785f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,7 @@ overrides: proxy-addr@>=1.1.0 <2.0.8: ^2.0.8 patchedDependencies: + '@adobe/fetch': fe94317880319fddf09a3d0f8eda551d5a2a205441bf5a7d634005239702b38d readabilitySAX@1.6.1: 13d40f78c0537e2538fd3a338f5ead833d968e2a80882e92f1eb3580b599c757 importers: @@ -40,7 +41,7 @@ importers: dependencies: '@adobe/fetch': specifier: ^4.1.11 - version: 4.1.11(supports-color@8.1.1) + version: 4.1.11(patch_hash=fe94317880319fddf09a3d0f8eda551d5a2a205441bf5a7d634005239702b38d)(supports-color@8.1.1) async: specifier: ^3.2.4 version: 3.2.4 @@ -1376,7 +1377,7 @@ packages: snapshots: - '@adobe/fetch@4.1.11(supports-color@8.1.1)': + '@adobe/fetch@4.1.11(patch_hash=fe94317880319fddf09a3d0f8eda551d5a2a205441bf5a7d634005239702b38d)(supports-color@8.1.1)': dependencies: debug: 4.4.0(supports-color@8.1.1) http-cache-semantics: 4.3.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c385fbc06..66711fa27 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -25,4 +25,5 @@ overrides: probe-image-size@<=7.3.0: ^7.4.0 proxy-addr@>=1.1.0 <2.0.8: ^2.0.8 patchedDependencies: + '@adobe/fetch': patches/@adobe__fetch.patch readabilitySAX@1.6.1: patches/readabilitySAX@1.6.1.patch From 3bf3eaed9f3612dd4a2dce0c82a812837ec917b3 Mon Sep 17 00:00:00 2001 From: Nazar Leush Date: Wed, 7 Oct 2026 18:12:31 +0300 Subject: [PATCH 06/18] debug log --- lib/fetch.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/fetch.js b/lib/fetch.js index 3c09cf36d..61da1f818 100644 --- a/lib/fetch.js +++ b/lib/fetch.js @@ -40,7 +40,8 @@ const fetchH1 = h1NoCache({ }).fetch; // Sorts AAAA first for this context only; autoSelectFamily falls back to IPv4 on connect failure. Requires Node >= 20.13. -const lookupIPv6First = (hostname, options, callback) => +const lookupIPv6First = (hostname, options, callback) => { + log(' -- lookupIPv6First start', hostname); dns.lookup(hostname, { ...options, order: 'ipv6first' }, (error, address, family) => { if (error) { log(' -- lookupIPv6First error', hostname, error.code, error.message); @@ -52,6 +53,7 @@ const lookupIPv6First = (hostname, options, callback) => } callback(error, address, family); }); +} const fetchIPv6First = noCache({ h1: { From 5fd23175075dd0bbc9815b2229b871f21a5016b4 Mon Sep 17 00:00:00 2001 From: Nazar Leush Date: Wed, 7 Oct 2026 19:05:22 +0300 Subject: [PATCH 07/18] allow set `prefer_ipv6` from main options --- lib/utils.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/utils.js b/lib/utils.js index 071dc8914..c45b918ba 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -171,6 +171,10 @@ export function prepareRequestOptions(request_options, options) { } } + if (options?.prefer_ipv6) { + request_options.prefer_ipv6 = true; + } + if (options?.user_agent) { // Let `options.user_agent` be prioritized over `proxy.user_agent`. request_options.headers['User-Agent'] = options?.user_agent; From ad4b8c4560bb2ff6c6e7124505e2e72c8084ec78 Mon Sep 17 00:00:00 2001 From: Nazar Leush Date: Wed, 7 Oct 2026 19:40:44 +0300 Subject: [PATCH 08/18] optimize different fetch cases as cached profiles by key --- lib/fetch.js | 124 +++++++++++++++++++++------------------------------ 1 file changed, 50 insertions(+), 74 deletions(-) diff --git a/lib/fetch.js b/lib/fetch.js index 61da1f818..773e72a09 100644 --- a/lib/fetch.js +++ b/lib/fetch.js @@ -4,74 +4,48 @@ import { h1NoCache, noCache, createUrl, AbortController, AbortError, FetchError import { serialize as serializeCookie, parse as parseCookie } from 'cookie'; import log from '../logging.js'; -const fetchKeepAlive = noCache({ - h1: { - keepAlive: true - }, - h2: { - enablePush: false - }, - rejectUnauthorized: false // By default skip auth check for all. -}).fetch; - -const fetchH1KeepAlive = h1NoCache({ - h1: { - keepAlive: true - }, - rejectUnauthorized: false // By default skip auth check for all. -}).fetch; - -const fetchAuthorized = noCache({ - h2: { - enablePush: false - } -}).fetch; // `rejectUnauthorized: true` - by `fetch` default. -const fetchH1Authorized = h1NoCache().fetch; // `rejectUnauthorized: true` - by `fetch` default. - -const { fetch } = noCache({ - h2: { - enablePush: false - }, - rejectUnauthorized: false // By default skip auth check for all. -}); - -const fetchH1 = h1NoCache({ - rejectUnauthorized: false // By default skip auth check for all. -}).fetch; - -// Sorts AAAA first for this context only; autoSelectFamily falls back to IPv4 on connect failure. Requires Node >= 20.13. -const lookupIPv6First = (hostname, options, callback) => { - log(' -- lookupIPv6First start', hostname); - dns.lookup(hostname, { ...options, order: 'ipv6first' }, (error, address, family) => { +const makeLookup = (order) => (hostname, options, callback) => + dns.lookup(hostname, { ...options, order }, (error, address, family) => { if (error) { - log(' -- lookupIPv6First error', hostname, error.code, error.message); + log(` -- lookup ${order} error`, hostname, error.code, error.message); } else if (Array.isArray(address)) { // With autoSelectFamily lookup is called with `all: true`. - log(' -- lookupIPv6First', hostname, address.map(a => `${a.address} (v${a.family})`).join(', ')); + log(` -- lookup ${order}`, hostname, address.map(a => `${a.address} (v${a.family})`).join(', ')); } else { - log(' -- lookupIPv6First', hostname, `${address} (v${family})`); + log(` -- lookup ${order}`, hostname, `${address} (v${family})`); } callback(error, address, family); }); -} -const fetchIPv6First = noCache({ - h1: { - lookup: lookupIPv6First - }, - h2: { - enablePush: false, - lookup: lookupIPv6First - }, - rejectUnauthorized: false // By default skip auth check for all. -}).fetch; - -const fetchH1IPv6First = h1NoCache({ - h1: { - lookup: lookupIPv6First - }, - rejectUnauthorized: false // By default skip auth check for all. -}).fetch; +// `ipv6first` order requires Node >= 20.13. autoSelectFamily falls back to the other family on connect failure. +const LOOKUPS = { + default: undefined, // Node's global resolver order. + ipv6first: makeLookup('ipv6first'), + ipv4first: makeLookup('ipv4first') +}; + +const PROFILES = { + default: { rejectUnauthorized: false }, // By default skip auth check for all. + keepAlive: { rejectUnauthorized: false, h1: { keepAlive: true } }, + authorized: {} // `rejectUnauthorized: true` - by `fetch` default. +}; + +// Lazily created fetch contexts, memoized per profile/protocol/dns order combination. +const fetchFuncs = {}; + +function getFetchFunc(profile, proto, order) { + const key = `${profile}:${proto}:${order}`; + if (!fetchFuncs[key]) { + const profileOpts = PROFILES[profile]; + const lookup = LOOKUPS[order]; + const h1 = { ...profileOpts.h1, ...(lookup && { lookup }) }; + const h2 = { enablePush: false, ...(lookup && { lookup }) }; + fetchFuncs[key] = proto === 'h1' + ? h1NoCache({ ...profileOpts, h1 }).fetch + : noCache({ ...profileOpts, h1, h2 }).fetch; + } + return fetchFuncs[key]; +} export function skipLoggingFetchError(error) { return error?.name === 'AbortError' @@ -84,14 +58,18 @@ export function skipLoggingFetchError(error) { || error?.code === 'ETIMEDOUT'; } -function selectFetchFunc(options, fetch_func = fetch, h1_fetch_func = fetchH1) { +function selectFetchFunc(options, profile = 'default') { + let order = 'default'; if (options.prefer_ipv6) { - return options.disable_http2 ? fetchH1IPv6First : fetchIPv6First; + order = 'ipv6first' + } + if (options.prefer_ipv4) { + order = 'ipv4first' } - return options.disable_http2 ? h1_fetch_func : fetch_func; + return getFetchFunc(profile, options.disable_http2 ? 'h1' : 'h2', order); } -function doFetch(fetch_func, h1_fetch_func, options) { +function doFetch(profile, options) { const fetch_options = Object.assign({}, options); const is_head_request = fetch_options.method === 'HEAD'; @@ -115,7 +93,7 @@ function doFetch(fetch_func, h1_fetch_func, options) { abortController.abort(); }, options.timeout || CONFIG.RESPONSE_TIMEOUT); - const a_fetch_func = selectFetchFunc(options, fetch_func, h1_fetch_func); + const a_fetch_func = selectFetchFunc(options, profile); return new Promise((resolve, reject) => { a_fetch_func(uri, fetch_options) .then(response => { @@ -141,7 +119,7 @@ function doFetch(fetch_func, h1_fetch_func, options) { && CONFIG.DISABLE_HTTP2_CHECKS?.some(check => typeof check === 'function' && check(response.status, headers))) { log(' -- doFetch check disabled h2', uri); - resolve(doFetch(fetch_func, h1_fetch_func, Object.assign({}, options, {disable_http2: true}))); + resolve(doFetch(profile, Object.assign({}, options, {disable_http2: true}))); } else { stream.status = response.status; if (response.url && response.url !== uri) { // Set as final destination url if Fetch follows 301/302 re-directs @@ -162,7 +140,7 @@ function doFetch(fetch_func, h1_fetch_func, options) { if (!options.disable_http2 && error.code && /^ERR_HTTP2/.test(error.code)) { log(' -- doFetch http2 error', error.code, uri); - resolve(doFetch(fetch_func, h1_fetch_func, Object.assign({}, options, {disable_http2: true}))); + resolve(doFetch(profile, Object.assign({}, options, {disable_http2: true}))); } else if (!options.disable_http2 && error.code && error instanceof FetchError && error.code === 'ABORT_ERR') { @@ -172,12 +150,12 @@ function doFetch(fetch_func, h1_fetch_func, options) { * https://app.everviz.com/show/O0Cy7Dyt */ log(' -- doFetch h2 aborted error', uri); - resolve(doFetch(fetch_func, h1_fetch_func, Object.assign({}, options, {disable_http2: true}))); + resolve(doFetch(profile, Object.assign({}, options, {disable_http2: true}))); } else if (!options.stopRecursion && CONFIG.ERRORS_TO_RETRY?.some(code => error.code?.indexOf(code) > -1)) { log(' -- doFetch ECONNRESET retry', error.code, uri); - resolve(doFetch(fetch_func, h1_fetch_func, Object.assign({}, options, {stopRecursion: true, disable_http2: true}))); + resolve(doFetch(profile, Object.assign({}, options, {stopRecursion: true, disable_http2: true}))); } else { if (error instanceof AbortError) { @@ -191,17 +169,15 @@ function doFetch(fetch_func, h1_fetch_func, options) { } export function fetchStreamKeepAlive(options) { - // TODO: ipv6 priority? - return doFetch(fetchKeepAlive, fetchH1KeepAlive, options); + return doFetch('keepAlive', options); } export function fetchStream(options) { - return doFetch(fetch, fetchH1, options); + return doFetch('default', options); }; export function fetchStreamAuthorized(options) { - // TODO: ipv6 priority? - return doFetch(fetchAuthorized, fetchH1Authorized, options); + return doFetch('authorized', options); }; export function fetchData(options) { From b45c95535de7bbebbffb762c915b0a24cbd40bbb Mon Sep 17 00:00:00 2001 From: Nazar Leush Date: Wed, 7 Oct 2026 19:45:57 +0300 Subject: [PATCH 09/18] rename `prefer_ipv6` to `ipv6first` --- lib/fetch.js | 8 ++++---- lib/utils.js | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/fetch.js b/lib/fetch.js index 773e72a09..c39e7252b 100644 --- a/lib/fetch.js +++ b/lib/fetch.js @@ -60,11 +60,11 @@ export function skipLoggingFetchError(error) { function selectFetchFunc(options, profile = 'default') { let order = 'default'; - if (options.prefer_ipv6) { - order = 'ipv6first' + if (options.ipv6first) { + order = 'ipv6first'; } - if (options.prefer_ipv4) { - order = 'ipv4first' + if (options.ipv4first) { + order = 'ipv4first'; } return getFetchFunc(profile, options.disable_http2 ? 'h1' : 'h2', order); } diff --git a/lib/utils.js b/lib/utils.js index c45b918ba..0ca748d7a 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -153,8 +153,8 @@ export function prepareRequestOptions(request_options, options) { if (proxy.disable_http2) { request_options.disable_http2 = true; } - if (proxy.prefer_ipv6) { - request_options.prefer_ipv6 = true; + if (proxy.ipv6first) { + request_options.ipv6first = true; } if (proxy.disable_language) { disable_language = true; @@ -171,8 +171,8 @@ export function prepareRequestOptions(request_options, options) { } } - if (options?.prefer_ipv6) { - request_options.prefer_ipv6 = true; + if (options?.ipv6first) { + request_options.ipv6first = true; } if (options?.user_agent) { From ca71efe9e1204155dba1a6e5815eb3a6b2c97a54 Mon Sep 17 00:00:00 2001 From: Nazar Leush Date: Wed, 7 Oct 2026 19:53:59 +0300 Subject: [PATCH 10/18] comment --- lib/fetch.js | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/lib/fetch.js b/lib/fetch.js index c39e7252b..1972ee90b 100644 --- a/lib/fetch.js +++ b/lib/fetch.js @@ -32,6 +32,46 @@ const PROFILES = { // Lazily created fetch contexts, memoized per profile/protocol/dns order combination. const fetchFuncs = {}; +/* +Generates fetches like: + +const fetchKeepAlive = noCache({ + h1: { + keepAlive: true + }, + h2: { + enablePush: false + }, + rejectUnauthorized: false // By default skip auth check for all. +}).fetch; + +const fetchH1KeepAlive = h1NoCache({ + h1: { + keepAlive: true + }, + rejectUnauthorized: false // By default skip auth check for all. +}).fetch; + +const fetchAuthorized = noCache({ + h2: { + enablePush: false + } +}).fetch; // `rejectUnauthorized: true` - by `fetch` default. + +const fetchH1Authorized = h1NoCache().fetch; + // `rejectUnauthorized: true` - by `fetch` default. + +const fetch = noCache({ + h2: { + enablePush: false + }, + rejectUnauthorized: false // By default skip auth check for all. +}).fetch; + +const fetchH1 = h1NoCache({ + rejectUnauthorized: false // By default skip auth check for all. +}).fetch; +*/ function getFetchFunc(profile, proto, order) { const key = `${profile}:${proto}:${order}`; From e59dcbd1ec5bb5f66bb0d890326a25153ec0fb61 Mon Sep 17 00:00:00 2001 From: Nazar Leush Date: Wed, 7 Oct 2026 20:03:02 +0300 Subject: [PATCH 11/18] make code nicer --- lib/fetch.js | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/lib/fetch.js b/lib/fetch.js index 1972ee90b..cce71b7a0 100644 --- a/lib/fetch.js +++ b/lib/fetch.js @@ -20,14 +20,27 @@ const makeLookup = (order) => (hostname, options, callback) => // `ipv6first` order requires Node >= 20.13. autoSelectFamily falls back to the other family on connect failure. const LOOKUPS = { default: undefined, // Node's global resolver order. - ipv6first: makeLookup('ipv6first'), - ipv4first: makeLookup('ipv4first') + ipv6first: { lookup: makeLookup('ipv6first') }, + ipv4first: { lookup: makeLookup('ipv4first') } }; +const PROFILE_DEFAULT = 'default'; +const PROFILE_KEEP_ALIVE = 'keepAlive'; +const PROFILE_AUTHORIZED = 'authorized'; + const PROFILES = { - default: { rejectUnauthorized: false }, // By default skip auth check for all. - keepAlive: { rejectUnauthorized: false, h1: { keepAlive: true } }, - authorized: {} // `rejectUnauthorized: true` - by `fetch` default. + [PROFILE_DEFAULT]: { + rejectUnauthorized: false, // By default skip auth check for all. + h2: { enablePush: false } + }, + [PROFILE_KEEP_ALIVE]: { + rejectUnauthorized: false, + h1: { keepAlive: true }, + h2: { enablePush: false } + }, + [PROFILE_AUTHORIZED]: { // `rejectUnauthorized: true` - by `fetch` default. + h2: { enablePush: false } + } }; // Lazily created fetch contexts, memoized per profile/protocol/dns order combination. @@ -78,8 +91,8 @@ function getFetchFunc(profile, proto, order) { if (!fetchFuncs[key]) { const profileOpts = PROFILES[profile]; const lookup = LOOKUPS[order]; - const h1 = { ...profileOpts.h1, ...(lookup && { lookup }) }; - const h2 = { enablePush: false, ...(lookup && { lookup }) }; + const h1 = { ...profileOpts.h1, ...lookup }; + const h2 = { ...profileOpts.h2, ...lookup }; fetchFuncs[key] = proto === 'h1' ? h1NoCache({ ...profileOpts, h1 }).fetch : noCache({ ...profileOpts, h1, h2 }).fetch; @@ -98,7 +111,7 @@ export function skipLoggingFetchError(error) { || error?.code === 'ETIMEDOUT'; } -function selectFetchFunc(options, profile = 'default') { +function selectFetchFunc(options, profile = PROFILE_DEFAULT) { let order = 'default'; if (options.ipv6first) { order = 'ipv6first'; @@ -209,15 +222,15 @@ function doFetch(profile, options) { } export function fetchStreamKeepAlive(options) { - return doFetch('keepAlive', options); + return doFetch(PROFILE_KEEP_ALIVE, options); } export function fetchStream(options) { - return doFetch('default', options); + return doFetch(PROFILE_DEFAULT, options); }; export function fetchStreamAuthorized(options) { - return doFetch('authorized', options); + return doFetch(PROFILE_AUTHORIZED, options); }; export function fetchData(options) { From 972d51ca8e4f39f730a9d68b7947813eab241aeb Mon Sep 17 00:00:00 2001 From: Nazar Leush Date: Wed, 7 Oct 2026 20:07:52 +0300 Subject: [PATCH 12/18] add ip order consts --- lib/fetch.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/fetch.js b/lib/fetch.js index cce71b7a0..1ae2b8639 100644 --- a/lib/fetch.js +++ b/lib/fetch.js @@ -18,10 +18,14 @@ const makeLookup = (order) => (hostname, options, callback) => }); // `ipv6first` order requires Node >= 20.13. autoSelectFamily falls back to the other family on connect failure. +const ORDER_DEFAULT = 'default'; +const ORDER_IPV6_FIRST = 'ipv6first'; +const ORDER_IPV4_FIRST = 'ipv4first'; + const LOOKUPS = { - default: undefined, // Node's global resolver order. - ipv6first: { lookup: makeLookup('ipv6first') }, - ipv4first: { lookup: makeLookup('ipv4first') } + [ORDER_DEFAULT]: undefined, // Node's global resolver order. + [ORDER_IPV6_FIRST]: { lookup: makeLookup(ORDER_IPV6_FIRST) }, + [ORDER_IPV4_FIRST]: { lookup: makeLookup(ORDER_IPV4_FIRST) } }; const PROFILE_DEFAULT = 'default'; @@ -112,12 +116,12 @@ export function skipLoggingFetchError(error) { } function selectFetchFunc(options, profile = PROFILE_DEFAULT) { - let order = 'default'; + let order = ORDER_DEFAULT; if (options.ipv6first) { - order = 'ipv6first'; + order = ORDER_IPV6_FIRST; } if (options.ipv4first) { - order = 'ipv4first'; + order = ORDER_IPV4_FIRST; } return getFetchFunc(profile, options.disable_http2 ? 'h1' : 'h2', order); } From 034417864a0f7baba616db003e48d68c58e407a0 Mon Sep 17 00:00:00 2001 From: Nazar Leush Date: Wed, 7 Oct 2026 20:21:46 +0300 Subject: [PATCH 13/18] simplify `fetch` call --- lib/fetch.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/fetch.js b/lib/fetch.js index 1ae2b8639..e6c948d3e 100644 --- a/lib/fetch.js +++ b/lib/fetch.js @@ -97,9 +97,7 @@ function getFetchFunc(profile, proto, order) { const lookup = LOOKUPS[order]; const h1 = { ...profileOpts.h1, ...lookup }; const h2 = { ...profileOpts.h2, ...lookup }; - fetchFuncs[key] = proto === 'h1' - ? h1NoCache({ ...profileOpts, h1 }).fetch - : noCache({ ...profileOpts, h1, h2 }).fetch; + fetchFuncs[key] = (proto === 'h1' ? h1NoCache : noCache)({ ...profileOpts, h1, h2 }).fetch; } return fetchFuncs[key]; } From 0630262ac90a09371d16fb257d255a44af180c50 Mon Sep 17 00:00:00 2001 From: Nazar Leush Date: Wed, 7 Oct 2026 20:37:26 +0300 Subject: [PATCH 14/18] support `options.ipv4first` --- lib/fetch.js | 3 +-- lib/utils.js | 4 ++++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/fetch.js b/lib/fetch.js index e6c948d3e..1b80ea8c5 100644 --- a/lib/fetch.js +++ b/lib/fetch.js @@ -117,8 +117,7 @@ function selectFetchFunc(options, profile = PROFILE_DEFAULT) { let order = ORDER_DEFAULT; if (options.ipv6first) { order = ORDER_IPV6_FIRST; - } - if (options.ipv4first) { + } else if (options.ipv4first) { order = ORDER_IPV4_FIRST; } return getFetchFunc(profile, options.disable_http2 ? 'h1' : 'h2', order); diff --git a/lib/utils.js b/lib/utils.js index 0ca748d7a..de2547b5b 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -155,6 +155,8 @@ export function prepareRequestOptions(request_options, options) { } if (proxy.ipv6first) { request_options.ipv6first = true; + } else if (proxy.ipv4first) { + request_options.ipv4first = true; } if (proxy.disable_language) { disable_language = true; @@ -173,6 +175,8 @@ export function prepareRequestOptions(request_options, options) { if (options?.ipv6first) { request_options.ipv6first = true; + } else if (options?.ipv4first) { + request_options.ipv4first = true; } if (options?.user_agent) { From f0197dffe1d39adbebf8d90d7f4daa2517f94949 Mon Sep 17 00:00:00 2001 From: Nazar Leush Date: Wed, 7 Oct 2026 20:45:27 +0300 Subject: [PATCH 15/18] add `FETCH_BY_PROTOCOL` --- lib/fetch.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/fetch.js b/lib/fetch.js index 1b80ea8c5..e2e66599b 100644 --- a/lib/fetch.js +++ b/lib/fetch.js @@ -47,6 +47,14 @@ const PROFILES = { } }; +const H1 = 'h1'; +const H2 = 'h2'; + +const FETCH_BY_PROTOCOL = { + [H1]: h1NoCache, + [H2]: noCache +}; + // Lazily created fetch contexts, memoized per profile/protocol/dns order combination. const fetchFuncs = {}; /* @@ -90,14 +98,16 @@ const fetchH1 = h1NoCache({ }).fetch; */ -function getFetchFunc(profile, proto, order) { - const key = `${profile}:${proto}:${order}`; +function getFetchFunc(profile, protocol, order) { + const key = `${profile}:${protocol}:${order}`; if (!fetchFuncs[key]) { const profileOpts = PROFILES[profile]; const lookup = LOOKUPS[order]; const h1 = { ...profileOpts.h1, ...lookup }; const h2 = { ...profileOpts.h2, ...lookup }; - fetchFuncs[key] = (proto === 'h1' ? h1NoCache : noCache)({ ...profileOpts, h1, h2 }).fetch; + const fetchOptions = { ...profileOpts, h1, h2 }; + const fetchFunc = FETCH_BY_PROTOCOL[protocol]; + fetchFuncs[key] = fetchFunc(fetchOptions).fetch; } return fetchFuncs[key]; } @@ -120,7 +130,7 @@ function selectFetchFunc(options, profile = PROFILE_DEFAULT) { } else if (options.ipv4first) { order = ORDER_IPV4_FIRST; } - return getFetchFunc(profile, options.disable_http2 ? 'h1' : 'h2', order); + return getFetchFunc(profile, options.disable_http2 ? H1 : H2, order); } function doFetch(profile, options) { From 945bf2c4e51ad5ed55510c2ac33a64c3a83500d0 Mon Sep 17 00:00:00 2001 From: Nazar Leush Date: Wed, 7 Oct 2026 22:37:38 +0300 Subject: [PATCH 16/18] read `providerOptions.app.ipv6first` --- lib/utils.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/utils.js b/lib/utils.js index de2547b5b..3e5735d72 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -126,6 +126,15 @@ export function prepareRequestOptions(request_options, options) { request_options.headers[key] = value; } } + + // Lowest priority for `ipv6first/ipv4first` from `providerOptions`. + if (options?.getProviderOptions) { + if (options.getProviderOptions('app.ipv6first')) { + request_options.ipv6first = true; + } else if (options.getProviderOptions('app.ipv4first')) { + request_options.ipv4first = true; + } + } if (CONFIG.PROXY || options?.proxy) { @@ -153,6 +162,7 @@ export function prepareRequestOptions(request_options, options) { if (proxy.disable_http2) { request_options.disable_http2 = true; } + // Middle priority for `ipv6first/ipv4first` from `proxy`. if (proxy.ipv6first) { request_options.ipv6first = true; } else if (proxy.ipv4first) { @@ -173,6 +183,7 @@ export function prepareRequestOptions(request_options, options) { } } + // Highest priority for `ipv6first/ipv4first` from `options`. if (options?.ipv6first) { request_options.ipv6first = true; } else if (options?.ipv4first) { From fbbda76e8267eadd208807b23b6960fb8ad22cfa Mon Sep 17 00:00:00 2001 From: Nazar Leush Date: Thu, 8 Oct 2026 16:06:23 +0300 Subject: [PATCH 17/18] fetch: debug connection --- lib/fetch.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/fetch.js b/lib/fetch.js index e2e66599b..d65a2e1ec 100644 --- a/lib/fetch.js +++ b/lib/fetch.js @@ -1,9 +1,20 @@ import { URL } from 'url'; import dns from 'node:dns'; +import diagnostics_channel from 'node:diagnostics_channel'; import { h1NoCache, noCache, createUrl, AbortController, AbortError, FetchError } from '@adobe/fetch'; import { serialize as serializeCookie, parse as parseCookie } from 'cookie'; import log from '../logging.js'; +// Log the actual IP each outgoing connection is made to (with autoSelectFamily it may differ from lookup order). +diagnostics_channel.subscribe('net.client.socket', ({ socket }) => { + socket.once('connect', () => { + // Skip non-http connections (redis, etc.). + if (socket.remotePort !== 443 && socket.remotePort !== 80) return; + const host = socket.servername || socket._host || ''; + log(` -- connected`, host, `${socket.remoteAddress} (${socket.remoteFamily}):${socket.remotePort}`); + }); +}); + const makeLookup = (order) => (hostname, options, callback) => dns.lookup(hostname, { ...options, order }, (error, address, family) => { if (error) { From 7282f1021274c00580484ef85aa3f9e4a3d0dd35 Mon Sep 17 00:00:00 2001 From: Nazar Leush Date: Thu, 8 Oct 2026 16:22:36 +0300 Subject: [PATCH 18/18] comment debug blocks --- lib/fetch.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/fetch.js b/lib/fetch.js index d65a2e1ec..c19d85349 100644 --- a/lib/fetch.js +++ b/lib/fetch.js @@ -1,11 +1,13 @@ import { URL } from 'url'; import dns from 'node:dns'; -import diagnostics_channel from 'node:diagnostics_channel'; import { h1NoCache, noCache, createUrl, AbortController, AbortError, FetchError } from '@adobe/fetch'; import { serialize as serializeCookie, parse as parseCookie } from 'cookie'; import log from '../logging.js'; -// Log the actual IP each outgoing connection is made to (with autoSelectFamily it may differ from lookup order). +/* +import diagnostics_channel from 'node:diagnostics_channel'; + +// Log the actual IP each outgoing connection is made to. diagnostics_channel.subscribe('net.client.socket', ({ socket }) => { socket.once('connect', () => { // Skip non-http connections (redis, etc.). @@ -14,7 +16,13 @@ diagnostics_channel.subscribe('net.client.socket', ({ socket }) => { log(` -- connected`, host, `${socket.remoteAddress} (${socket.remoteFamily}):${socket.remotePort}`); }); }); +*/ +const makeLookup = (order) => (hostname, options, callback) => + dns.lookup(hostname, { ...options, order }, callback); + +// Debug lookup. +/* const makeLookup = (order) => (hostname, options, callback) => dns.lookup(hostname, { ...options, order }, (error, address, family) => { if (error) { @@ -27,6 +35,7 @@ const makeLookup = (order) => (hostname, options, callback) => } callback(error, address, family); }); +*/ // `ipv6first` order requires Node >= 20.13. autoSelectFamily falls back to the other family on connect failure. const ORDER_DEFAULT = 'default';