From 34cf87517a5b9f429fd766a25ff934b3124fe9fe Mon Sep 17 00:00:00 2001 From: khhjoe Date: Tue, 17 Mar 2026 18:55:07 +0800 Subject: [PATCH] fix(oauth): proxy-aware fetch for OpenAI Codex token exchange Node.js native fetch() ignores HTTP_PROXY / HTTPS_PROXY env vars. Users behind a proxy (e.g. Hong Kong, mainland China) get 403 'unsupported_country_region_territory' during token exchange even though the browser OAuth flow succeeds through their system proxy. Wrap the loginOpenAICodex() call with withProxyFetch(), which temporarily patches globalThis.fetch with an undici ProxyAgent-backed implementation when proxy env vars are detected. The original fetch is restored after the call completes. No-op when no proxy is configured or undici is unavailable. Fixes #29418 --- src/plugins/provider-openai-codex-oauth.ts | 57 ++++++++++++++++++++-- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/src/plugins/provider-openai-codex-oauth.ts b/src/plugins/provider-openai-codex-oauth.ts index 6e16cf863f0..b6fcba2301e 100644 --- a/src/plugins/provider-openai-codex-oauth.ts +++ b/src/plugins/provider-openai-codex-oauth.ts @@ -7,6 +7,51 @@ import { runOpenAIOAuthTlsPreflight, } from "./provider-openai-codex-oauth-tls.js"; +/** + * Node.js native fetch() ignores HTTP_PROXY / HTTPS_PROXY env vars. + * This helper temporarily patches globalThis.fetch with a proxy-aware + * implementation (via undici ProxyAgent) for the duration of `fn()`, + * then restores the original fetch. No-op when no proxy is configured + * or undici is unavailable. + */ +async function withProxyFetch(fn: () => Promise): Promise { + const proxyUrl = + process.env.HTTPS_PROXY || + process.env.https_proxy || + process.env.HTTP_PROXY || + process.env.http_proxy || + process.env.ALL_PROXY || + process.env.all_proxy; + if (!proxyUrl) return fn(); + + let restore: (() => void) | undefined; + try { + const { createRequire } = await import("node:module"); + const require_ = createRequire(import.meta.url); + // undici is a transitive dependency of OpenClaw (via Node internals / direct dep) + // eslint-disable-next-line @typescript-eslint/no-require-imports + const undici = require_("undici") as typeof import("undici"); + const agent = new undici.ProxyAgent(proxyUrl); + const origFetch = globalThis.fetch; + globalThis.fetch = ((url: Parameters[0], init?: Parameters[1]) => + undici.fetch(url as Parameters[0], { + ...(init as Parameters[1]), + dispatcher: agent, + })) as typeof fetch; + restore = () => { + globalThis.fetch = origFetch; + }; + } catch { + // undici not available — proceed with unpatched fetch + } + + try { + return await fn(); + } finally { + restore?.(); + } +} + export async function loginOpenAICodexOAuth(params: { prompter: WizardPrompter; runtime: RuntimeEnv; @@ -49,11 +94,13 @@ export async function loginOpenAICodexOAuth(params: { localBrowserMessage: localBrowserMessage ?? "Complete sign-in in browser…", }); - const creds = await loginOpenAICodex({ - onAuth: baseOnAuth, - onPrompt, - onProgress: (msg: string) => spin.update(msg), - }); + const creds = await withProxyFetch(() => + loginOpenAICodex({ + onAuth: baseOnAuth, + onPrompt, + onProgress: (msg: string) => spin.update(msg), + }), + ); spin.stop("OpenAI OAuth complete"); return creds ?? null; } catch (err) {