fix(extension): detect browser error pages earlier
Testing / test (push) Successful in 26s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 27s
Testing / remote-protocol-compat (0.9.5) (push) Successful in 20s
Package Extension / package-extension (push) Successful in 28s
Build & Publish Package / publish (push) Successful in 31s

- 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.
This commit is contained in:
2026-05-14 13:54:21 +02:00
parent f79ff0e3c2
commit eaa1469143
8 changed files with 61 additions and 15 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "browser-cli", "name": "browser-cli",
"version": "0.9.6", "version": "0.9.8",
"description": "Control your browser from the terminal via browser-cli", "description": "Control your browser from the terminal via browser-cli",
"permissions": [ "permissions": [
"tabs", "tabs",
+13 -2
View File
@@ -1,5 +1,5 @@
// @ts-nocheck // @ts-nocheck
import { executeScript, getActiveTab, isErrorPageScriptError, isScriptableUrl } from '../core'; import { executeScript, getActiveTab, isBrowserErrorUrl, isErrorPageScriptError, isScriptableUrl } from '../core';
import { contentDispatch } from './injected'; import { contentDispatch } from './injected';
function fallbackForErrorPageDomOp(funcName, tab) { function fallbackForErrorPageDomOp(funcName, tab) {
@@ -31,6 +31,10 @@ function fallbackForErrorPageDomOp(funcName, tab) {
export async function domOp(funcName, args = {}) { export async function domOp(funcName, args = {}) {
const tab = args?.tabId ? await chrome.tabs.get(args.tabId) : await getActiveTab(); const tab = args?.tabId ? await chrome.tabs.get(args.tabId) : await getActiveTab();
const tabUrl = tab.url || tab.pendingUrl || ""; const tabUrl = tab.url || tab.pendingUrl || "";
if (isBrowserErrorUrl(tabUrl)) {
const fallback = fallbackForErrorPageDomOp(funcName, tab);
if (fallback !== undefined) return fallback;
}
if (!isScriptableUrl(tabUrl)) { if (!isScriptableUrl(tabUrl)) {
throw new Error(`Cannot run DOM commands on ${tabUrl} — navigate to a regular web page first`); 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 } = {}) { export async function domEval({ code, tabId } = {}) {
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab(); const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
const tabUrl = tab.url || tab.pendingUrl || ""; 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`); throw new Error(`Cannot run DOM commands on ${tabUrl} — navigate to a regular web page first`);
} }
const results = await executeScript({ 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 } = {}) { export async function domWaitFor({ selector, timeout = 10000, visible = false, hidden = false, tabId } = {}) {
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab(); const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
const tabUrl = tab.url || tab.pendingUrl || ""; 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)) { if (!isScriptableUrl(tabUrl)) {
throw new Error(`Cannot run DOM commands on ${tabUrl} — navigate to a regular web page first`); 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 } = {}) { export async function domPoll({ selector, pattern, attr, timeout = 30000, interval = 500, tabId } = {}) {
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab(); const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
const tabUrl = tab.url || tab.pendingUrl || ""; const tabUrl = tab.url || tab.pendingUrl || "";
if (isBrowserErrorUrl(tabUrl)) {
throw new Error(`Cannot poll DOM on browser error page ${tabUrl}`);
}
if (!isScriptableUrl(tabUrl)) { if (!isScriptableUrl(tabUrl)) {
throw new Error(`Cannot run DOM commands on ${tabUrl} — navigate to a regular web page first`); throw new Error(`Cannot run DOM commands on ${tabUrl} — navigate to a regular web page first`);
} }
+5 -1
View File
@@ -1,5 +1,5 @@
// @ts-nocheck // @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 }) { export async function navOpen({ url, background, window: windowName, windowId: explicitWindowId, group: groupNameOrId }) {
let windowId; let windowId;
if (explicitWindowId != null) { if (explicitWindowId != null) {
@@ -77,6 +77,10 @@ export async function navWait({ tabId, timeout = 30000, readyState = "complete"
const interval = 200; const interval = 200;
while (Date.now() < deadline) { while (Date.now() < deadline) {
const t = await chrome.tabs.get(tab.id); 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") { if (readyState === "complete" ? t.status === "complete" : t.status !== "loading") {
return tabInfo(t); return tabInfo(t);
} }
+10 -5
View File
@@ -1,5 +1,5 @@
// @ts-nocheck // @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() { export async function tabsList() {
const windows = await chrome.windows.getAll({ populate: true }); const windows = await chrome.windows.getAll({ populate: true });
const aliases = await getAliases(); const aliases = await getAliases();
@@ -102,8 +102,12 @@ export async function tabsQuery({ search }) {
export async function tabsHtml({ tabId }) { export async function tabsHtml({ tabId }) {
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab(); const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
if (!isScriptableUrl(tab.url || tab.pendingUrl || "")) { const tabUrl = tab.url || tab.pendingUrl || "";
throw new Error(`Cannot get HTML of ${tab.url || tab.pendingUrl} — navigate to a regular web page first`); if (isBrowserErrorUrl(tabUrl)) {
return "";
}
if (!isScriptableUrl(tabUrl)) {
throw new Error(`Cannot get HTML of ${tabUrl} — navigate to a regular web page first`);
} }
try { try {
const results = await executeScript({ const results = await executeScript({
@@ -112,7 +116,8 @@ export async function tabsHtml({ tabId }) {
}); });
return results[0]?.result || ""; return results[0]?.result || "";
} catch (e) { } 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) { if (i < 2 && transient) {
await new Promise(r => setTimeout(r, 300)); await new Promise(r => setTimeout(r, 300));
continue; continue;
@@ -205,7 +210,7 @@ export async function tabsWatchUrl({ pattern, timeout = 30000, tabId } = {}) {
lastUrl = t.url || t.pendingUrl || ""; lastUrl = t.url || t.pendingUrl || "";
lastStatus = t.status || "unknown"; lastStatus = t.status || "unknown";
if (matches(t.pendingUrl || "") || matches(t.url || "")) return tabInfo(t); 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}'`); 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)); await new Promise(r => setTimeout(r, 200));
+19 -2
View File
@@ -5,9 +5,26 @@ export async function getProfileAlias() {
return profileAlias || "default"; 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) { export function isErrorPageScriptError(error) {
const message = String(error?.message || error || ""); const message = String(error?.message || error || "").toLowerCase();
return message.includes("error page") || message.includes("chrome-error://chromewebdata"); 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) { export function isTransientScriptError(error) {
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "browser-cli" name = "browser-cli"
version = "0.9.6" version = "0.9.8"
description = "Control your real running browser from the terminal via a browser extension" description = "Control your real running browser from the terminal via a browser extension"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
+11 -2
View File
@@ -5,8 +5,13 @@ ROOT = Path(__file__).resolve().parents[1]
def test_extension_retries_error_page_script_injection_before_failing(): def test_extension_retries_error_page_script_injection_before_failing():
core = (ROOT / "extension" / "src" / "core.ts").read_text() core = (ROOT / "extension" / "src" / "core.ts").read_text()
assert "isBrowserErrorUrl" in core
assert "isErrorPageScriptError" 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 assert "isTransientScriptError(e)" in core
def test_read_only_dom_commands_have_error_page_fallbacks(): 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 "return false;" in dom
assert 'case "domQuery":' in dom assert 'case "domQuery":' in dom
assert 'case "extractText":' in dom assert 'case "extractText":' in dom
assert "isBrowserErrorUrl(tabUrl)" in dom
assert "isErrorPageScriptError(e)" 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() tabs = (ROOT / "extension" / "src" / "commands" / "tabs.ts").read_text()
navigation = (ROOT / "extension" / "src" / "commands" / "navigation.ts").read_text()
assert "lastUrl" in tabs assert "lastUrl" in tabs
assert "lastStatus" in tabs assert "lastStatus" in tabs
assert "showing an error page" in tabs assert "showing an error page" in tabs
assert "last URL:" in tabs assert "last URL:" in tabs
assert "isBrowserErrorUrl" in navigation
assert "showing an error page while waiting for load" in navigation
Generated
+1 -1
View File
@@ -4,7 +4,7 @@ requires-python = ">=3.10"
[[package]] [[package]]
name = "browser-cli" name = "browser-cli"
version = "0.9.6" version = "0.9.8"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "click" }, { name = "click" },