From eaa1469143a0514a11c1a769d0b926dae335b068 Mon Sep 17 00:00:00 2001 From: Daniel Dolezal Date: Thu, 14 May 2026 13:54:21 +0200 Subject: [PATCH] fix(extension): detect browser error pages earlier - Add shared browser error URL detection for Chrome, Edge, Brave, and Firefox-style about:error pages. - Short-circuit read-only DOM and HTML commands with safe fallbacks when tabs are already on browser error pages. - Fail navigation waits, DOM waits, polling, and URL watches with clearer error-page messages. - Bump package and extension version to 0.9.8 and extend regression coverage for cross-browser error-page handling. --- extension/manifest.json | 2 +- extension/src/commands/dom.ts | 15 +++++++++++++-- extension/src/commands/navigation.ts | 6 +++++- extension/src/commands/tabs.ts | 15 ++++++++++----- extension/src/core.ts | 21 +++++++++++++++++++-- pyproject.toml | 2 +- tests/test_extension_error_page_handling.py | 13 +++++++++++-- uv.lock | 2 +- 8 files changed, 61 insertions(+), 15 deletions(-) diff --git a/extension/manifest.json b/extension/manifest.json index ea4dba9..e07ecd0 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "browser-cli", - "version": "0.9.6", + "version": "0.9.8", "description": "Control your browser from the terminal via browser-cli", "permissions": [ "tabs", diff --git a/extension/src/commands/dom.ts b/extension/src/commands/dom.ts index 910180e..c9bffae 100644 --- a/extension/src/commands/dom.ts +++ b/extension/src/commands/dom.ts @@ -1,5 +1,5 @@ // @ts-nocheck -import { executeScript, getActiveTab, isErrorPageScriptError, isScriptableUrl } from '../core'; +import { executeScript, getActiveTab, isBrowserErrorUrl, isErrorPageScriptError, isScriptableUrl } from '../core'; import { contentDispatch } from './injected'; function fallbackForErrorPageDomOp(funcName, tab) { @@ -31,6 +31,10 @@ function fallbackForErrorPageDomOp(funcName, tab) { export async function domOp(funcName, args = {}) { const tab = args?.tabId ? await chrome.tabs.get(args.tabId) : await getActiveTab(); const tabUrl = tab.url || tab.pendingUrl || ""; + if (isBrowserErrorUrl(tabUrl)) { + const fallback = fallbackForErrorPageDomOp(funcName, tab); + if (fallback !== undefined) return fallback; + } if (!isScriptableUrl(tabUrl)) { throw new Error(`Cannot run DOM commands on ${tabUrl} — navigate to a regular web page first`); } @@ -53,7 +57,7 @@ export async function domOp(funcName, args = {}) { export async function domEval({ code, tabId } = {}) { const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab(); const tabUrl = tab.url || tab.pendingUrl || ""; - if (!isScriptableUrl(tabUrl)) { + if (!isScriptableUrl(tabUrl) || isBrowserErrorUrl(tabUrl)) { throw new Error(`Cannot run DOM commands on ${tabUrl} — navigate to a regular web page first`); } const results = await executeScript({ @@ -68,6 +72,10 @@ export async function domEval({ code, tabId } = {}) { export async function domWaitFor({ selector, timeout = 10000, visible = false, hidden = false, tabId } = {}) { const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab(); const tabUrl = tab.url || tab.pendingUrl || ""; + if (isBrowserErrorUrl(tabUrl)) { + if (hidden) return { selector, found: false }; + throw new Error(`Cannot wait for DOM on browser error page ${tabUrl}`); + } if (!isScriptableUrl(tabUrl)) { throw new Error(`Cannot run DOM commands on ${tabUrl} — navigate to a regular web page first`); } @@ -101,6 +109,9 @@ export async function domWaitFor({ selector, timeout = 10000, visible = false, h export async function domPoll({ selector, pattern, attr, timeout = 30000, interval = 500, tabId } = {}) { const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab(); const tabUrl = tab.url || tab.pendingUrl || ""; + if (isBrowserErrorUrl(tabUrl)) { + throw new Error(`Cannot poll DOM on browser error page ${tabUrl}`); + } if (!isScriptableUrl(tabUrl)) { throw new Error(`Cannot run DOM commands on ${tabUrl} — navigate to a regular web page first`); } diff --git a/extension/src/commands/navigation.ts b/extension/src/commands/navigation.ts index c605243..168c92c 100644 --- a/extension/src/commands/navigation.ts +++ b/extension/src/commands/navigation.ts @@ -1,5 +1,5 @@ // @ts-nocheck -import { getActiveTab, getAliases, resolveGroupId, tabInfo } from '../core'; +import { getActiveTab, getAliases, isBrowserErrorUrl, resolveGroupId, tabInfo } from '../core'; export async function navOpen({ url, background, window: windowName, windowId: explicitWindowId, group: groupNameOrId }) { let windowId; if (explicitWindowId != null) { @@ -77,6 +77,10 @@ export async function navWait({ tabId, timeout = 30000, readyState = "complete" const interval = 200; while (Date.now() < deadline) { const t = await chrome.tabs.get(tab.id); + const currentUrl = t.url || t.pendingUrl || ""; + if (isBrowserErrorUrl(currentUrl)) { + throw new Error(`Tab ${tab.id} is showing an error page while waiting for load (${currentUrl})`); + } if (readyState === "complete" ? t.status === "complete" : t.status !== "loading") { return tabInfo(t); } diff --git a/extension/src/commands/tabs.ts b/extension/src/commands/tabs.ts index e5e2f9f..8e4692c 100644 --- a/extension/src/commands/tabs.ts +++ b/extension/src/commands/tabs.ts @@ -1,5 +1,5 @@ // @ts-nocheck -import { executeScript, getActiveTab, getAliases, isScriptableUrl, resolveTabForDirectAction, tabInfo } from '../core'; +import { executeScript, getActiveTab, getAliases, isBrowserErrorUrl, isErrorPageScriptError, isScriptableUrl, resolveTabForDirectAction, tabInfo } from '../core'; export async function tabsList() { const windows = await chrome.windows.getAll({ populate: true }); const aliases = await getAliases(); @@ -102,8 +102,12 @@ export async function tabsQuery({ search }) { export async function tabsHtml({ tabId }) { for (let i = 0; i < 3; i++) { const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab(); - if (!isScriptableUrl(tab.url || tab.pendingUrl || "")) { - throw new Error(`Cannot get HTML of ${tab.url || tab.pendingUrl} — navigate to a regular web page first`); + const tabUrl = tab.url || tab.pendingUrl || ""; + if (isBrowserErrorUrl(tabUrl)) { + return ""; + } + if (!isScriptableUrl(tabUrl)) { + throw new Error(`Cannot get HTML of ${tabUrl} — navigate to a regular web page first`); } try { const results = await executeScript({ @@ -112,7 +116,8 @@ export async function tabsHtml({ tabId }) { }); return results[0]?.result || ""; } catch (e) { - const transient = e.message && (e.message.includes("Frame with ID") || e.message.includes("No tab with id")) && !e.message.includes("error page"); + if (isErrorPageScriptError(e)) return ""; + const transient = e.message && (e.message.includes("Frame with ID") || e.message.includes("No tab with id")); if (i < 2 && transient) { await new Promise(r => setTimeout(r, 300)); continue; @@ -205,7 +210,7 @@ export async function tabsWatchUrl({ pattern, timeout = 30000, tabId } = {}) { lastUrl = t.url || t.pendingUrl || ""; lastStatus = t.status || "unknown"; if (matches(t.pendingUrl || "") || matches(t.url || "")) return tabInfo(t); - if ((t.url || "").startsWith("chrome-error://")) { + if (isBrowserErrorUrl(t.url || "")) { throw new Error(`Tab ${tab.id} is showing an error page while waiting for URL to match '${pattern}'`); } await new Promise(r => setTimeout(r, 200)); diff --git a/extension/src/core.ts b/extension/src/core.ts index e8502f8..8b21d53 100644 --- a/extension/src/core.ts +++ b/extension/src/core.ts @@ -5,9 +5,26 @@ export async function getProfileAlias() { return profileAlias || "default"; } +export function isBrowserErrorUrl(url) { + const value = String(url || "").toLowerCase(); + return value.startsWith("chrome-error://") || + value.startsWith("edge-error://") || + value.startsWith("brave-error://") || + value.startsWith("about:neterror") || + value.startsWith("about:certerror") || + value.startsWith("about:blocked") || + value.startsWith("about:tabcrashed"); +} + export function isErrorPageScriptError(error) { - const message = String(error?.message || error || ""); - return message.includes("error page") || message.includes("chrome-error://chromewebdata"); + const message = String(error?.message || error || "").toLowerCase(); + return message.includes("error page") || + message.includes("chrome-error://") || + message.includes("edge-error://") || + message.includes("brave-error://") || + message.includes("about:neterror") || + message.includes("about:certerror") || + message.includes("about:tabcrashed"); } export function isTransientScriptError(error) { diff --git a/pyproject.toml b/pyproject.toml index 75e5ebb..d15b67b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "browser-cli" -version = "0.9.6" +version = "0.9.8" description = "Control your real running browser from the terminal via a browser extension" requires-python = ">=3.10" dependencies = [ diff --git a/tests/test_extension_error_page_handling.py b/tests/test_extension_error_page_handling.py index 2016f2b..1fbe258 100644 --- a/tests/test_extension_error_page_handling.py +++ b/tests/test_extension_error_page_handling.py @@ -5,8 +5,13 @@ ROOT = Path(__file__).resolve().parents[1] def test_extension_retries_error_page_script_injection_before_failing(): core = (ROOT / "extension" / "src" / "core.ts").read_text() + assert "isBrowserErrorUrl" in core assert "isErrorPageScriptError" in core - assert "chrome-error://chromewebdata" in core + assert "chrome-error://" in core + assert "edge-error://" in core + assert "brave-error://" in core + assert "about:neterror" in core + assert "about:certerror" in core assert "isTransientScriptError(e)" in core def test_read_only_dom_commands_have_error_page_fallbacks(): @@ -17,12 +22,16 @@ def test_read_only_dom_commands_have_error_page_fallbacks(): assert "return false;" in dom assert 'case "domQuery":' in dom assert 'case "extractText":' in dom + assert "isBrowserErrorUrl(tabUrl)" in dom assert "isErrorPageScriptError(e)" in dom -def test_tabs_watch_url_reports_last_seen_url_on_timeout(): +def test_navigation_and_tabs_report_browser_error_pages(): tabs = (ROOT / "extension" / "src" / "commands" / "tabs.ts").read_text() + navigation = (ROOT / "extension" / "src" / "commands" / "navigation.ts").read_text() assert "lastUrl" in tabs assert "lastStatus" in tabs assert "showing an error page" in tabs assert "last URL:" in tabs + assert "isBrowserErrorUrl" in navigation + assert "showing an error page while waiting for load" in navigation diff --git a/uv.lock b/uv.lock index f84ba30..e5d41cb 100644 --- a/uv.lock +++ b/uv.lock @@ -4,7 +4,7 @@ requires-python = ">=3.10" [[package]] name = "browser-cli" -version = "0.9.6" +version = "0.9.8" source = { editable = "." } dependencies = [ { name = "click" },