From 274ccfbc01ae0f7eb192a1a990a602cba4d875ab Mon Sep 17 00:00:00 2001 From: Gerhard Scheikl Date: Sat, 9 May 2026 22:26:04 +0200 Subject: [PATCH] attempt to fix mail sending --- .vscode/settings.json | 3 +- app/services/invoice/safeFetch.server.ts | 37 ++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 7551392..7a5ff45 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "chat.tools.terminal.autoApprove": { "setopt": true, - "npx shopify": true + "npx shopify": true, + "npx tsx": true } } \ No newline at end of file diff --git a/app/services/invoice/safeFetch.server.ts b/app/services/invoice/safeFetch.server.ts index aa20a9d..df906cc 100644 --- a/app/services/invoice/safeFetch.server.ts +++ b/app/services/invoice/safeFetch.server.ts @@ -170,11 +170,42 @@ export async function safeFetch(rawUrl: string, opts: SafeFetchOptions = {}): Pr // Pin the resolved IP. We pass an Agent with a custom `lookup` that always // returns our pre-validated address, so the actual TCP connect can't be // re-resolved to something else (DNS-rebinding defense). + // + // Note: Node 20+ enables Happy Eyeballs (`autoSelectFamily: true`) by + // default on the http/https agents. Happy Eyeballs calls `lookup` with + // `{ all: true }` and expects the callback to receive an *array* of + // `{ address, family }` records. If we ignore that and always invoke the + // 3-arg form, the connector hands `undefined` to `socket.connect()`, + // which then throws `Invalid IP address: undefined`. + type LookupCb = + | ((err: NodeJS.ErrnoException | null, address: string, family: number) => void) + | ((err: NodeJS.ErrnoException | null, addresses: { address: string; family: number }[]) => void); const pinnedLookup = ( _hostname: string, - _options: unknown, - cb: (err: NodeJS.ErrnoException | null, address: string, family: number) => void, - ) => cb(null, address, family); + optionsOrCb: { all?: boolean; family?: number } | LookupCb, + maybeCb?: LookupCb, + ) => { + let options: { all?: boolean; family?: number } = {}; + let cb: LookupCb; + if (typeof optionsOrCb === "function") { + cb = optionsOrCb; + } else { + options = optionsOrCb ?? {}; + cb = maybeCb as LookupCb; + } + if (options.all) { + (cb as (err: NodeJS.ErrnoException | null, addresses: { address: string; family: number }[]) => void)( + null, + [{ address, family }], + ); + } else { + (cb as (err: NodeJS.ErrnoException | null, address: string, family: number) => void)( + null, + address, + family, + ); + } + }; const isHttps = url.protocol === "https:"; const agent = isHttps