From 0484a44d55047072106f8e47ecfce9551eaf4a0f Mon Sep 17 00:00:00 2001 From: Sam Cui Date: Fri, 3 Apr 2026 13:09:03 +0800 Subject: [PATCH] fix: bypass CORS for site adapters via about:blank execution + --disable-web-security MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Site adapters that make cross-origin API calls (e.g. hackernews/top fetching from firebaseio.com, bbc/news fetching from feeds.bbci.co.uk) fail with "TypeError: Failed to fetch" because Runtime.evaluate runs in page context where browser CORS restrictions apply. Two complementary fixes: 1. Add --disable-web-security to managed browser launch args. This flag only affects the dedicated bb-browser Chrome instance (separate user-data-dir), not the user's daily browser. On about:blank tabs (null origin), this completely eliminates CORS enforcement. 2. Route adapters with capabilities: ["network"] to an about:blank tab instead of the target domain tab. Combined with (1), cross-origin fetch() calls succeed without CORS restrictions. Adapters requiring cookies (same-origin) continue to run on the domain tab as before. 3. Auto-retry fallback: if any adapter fails with "Failed to fetch" on a domain tab, automatically retry on an about:blank tab. This catches adapters that make cross-origin calls but don't declare capabilities: ["network"]. Tested: hackernews/top, bbc/news, arxiv/search, stackoverflow/search, wikipedia/summary — all return valid JSON after this fix. Fixes #41 Closes #110 (跨域 fetch 部分) Related: #104, #45 Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/cli/src/cdp-discovery.ts | 1 + packages/cli/src/commands/site.ts | 54 +++++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/cdp-discovery.ts b/packages/cli/src/cdp-discovery.ts index 235404cc..a43f69e8 100644 --- a/packages/cli/src/cdp-discovery.ts +++ b/packages/cli/src/cdp-discovery.ts @@ -175,6 +175,7 @@ export async function launchManagedBrowser(port: number = DEFAULT_CDP_PORT): Pro "--disable-background-networking", "--disable-component-update", "--disable-features=Translate,MediaRouter", + "--disable-web-security", "--disable-session-crashed-bubble", "--hide-crash-restore-bubble", "about:blank", diff --git a/packages/cli/src/commands/site.ts b/packages/cli/src/commands/site.ts index 58363883..c77db9ac 100644 --- a/packages/cli/src/commands/site.ts +++ b/packages/cli/src/commands/site.ts @@ -648,8 +648,34 @@ async function siteRun( // 确定目标 tab let targetTabId: number | undefined = options.tabId; - // 如果用户没指定 --tab,自动查找匹配域名的 tab - if (!targetTabId && site.domain) { + // Adapters with "network" capability (and without "cookie") make cross-origin + // API calls that fail under page-context CORS. Execute them on an about:blank + // tab where the managed browser's --disable-web-security flag takes effect. + const hasNetworkCap = Array.isArray(site.capabilities) && site.capabilities.includes("network"); + const hasCookieCap = Array.isArray(site.capabilities) && site.capabilities.includes("cookie"); + const useBlankTab = hasNetworkCap && !hasCookieCap && !targetTabId; + + if (useBlankTab) { + // Find or create an about:blank tab for CORS-free execution + const listReq: Request = { id: generateId(), action: "tab_list" }; + const listResp: Response = await sendCommand(listReq); + if (listResp.success && listResp.data?.tabs) { + const blankTab = listResp.data.tabs.find((tab: TabInfo) => tab.url === "about:blank"); + if (blankTab) { + targetTabId = blankTab.tabId; + } + } + if (!targetTabId) { + const newResp = await sendCommand({ + id: generateId(), + action: "tab_new", + url: "about:blank", + }); + targetTabId = newResp.data?.tabId; + await new Promise((resolve) => setTimeout(resolve, 500)); + } + } else if (!targetTabId && site.domain) { + // 如果用户没指定 --tab,自动查找匹配域名的 tab const listReq: Request = { id: generateId(), action: "tab_list" }; const listResp: Response = await sendCommand(listReq); @@ -675,7 +701,29 @@ async function siteRun( // 执行 const evalReq: Request = { id: generateId(), action: "eval", script, tabId: targetTabId }; - const evalResp: Response = await sendCommand(evalReq); + let evalResp: Response = await sendCommand(evalReq); + + // Fallback: if eval failed with "Failed to fetch" on a domain tab, retry on + // about:blank where --disable-web-security eliminates CORS restrictions. + // This catches adapters that lack capabilities: ["network"] but still make + // cross-origin API calls. + if (!evalResp.success && /Failed to fetch/i.test(evalResp.error || "") && !useBlankTab) { + let blankTabId: string | undefined; + const listReq: Request = { id: generateId(), action: "tab_list" }; + const listResp: Response = await sendCommand(listReq); + if (listResp.success && listResp.data?.tabs) { + const blankTab = listResp.data.tabs.find((tab: TabInfo) => tab.url === "about:blank"); + if (blankTab) blankTabId = blankTab.tabId; + } + if (!blankTabId) { + const newResp = await sendCommand({ id: generateId(), action: "tab_new", url: "about:blank" }); + blankTabId = newResp.data?.tabId; + await new Promise((resolve) => setTimeout(resolve, 500)); + } + if (blankTabId) { + evalResp = await sendCommand({ id: generateId(), action: "eval", script, tabId: blankTabId }); + } + } if (!evalResp.success) { const hint = site.domain