fix(cli): improve browser control errors

This commit is contained in:
Peter Steinberger
2025-12-13 17:37:00 +00:00
parent 4228ee326c
commit 9be3394bac
6 changed files with 233 additions and 92 deletions

View File

@@ -0,0 +1,24 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { browserStatus } from "./client.js";
describe("browser client", () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it("wraps connection failures with a gateway hint", async () => {
const refused = Object.assign(new Error("connect ECONNREFUSED 127.0.0.1"), {
code: "ECONNREFUSED",
});
const fetchFailed = Object.assign(new TypeError("fetch failed"), {
cause: refused,
});
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(fetchFailed));
await expect(browserStatus("http://127.0.0.1:18791")).rejects.toThrow(
/Start .*gateway/i,
);
});
});

View File

@@ -28,6 +28,52 @@ export type ScreenshotResult = {
url: string;
};
function unwrapCause(err: unknown): unknown {
if (!err || typeof err !== "object") return null;
const cause = (err as { cause?: unknown }).cause;
return cause ?? null;
}
function enhanceBrowserFetchError(
url: string,
err: unknown,
timeoutMs: number,
): Error {
const cause = unwrapCause(err);
const code =
(cause && typeof cause === "object" && "code" in cause
? String((cause as { code?: unknown }).code ?? "")
: "") ||
(err && typeof err === "object" && "code" in err
? String((err as { code?: unknown }).code ?? "")
: "");
const hint =
"Start (or restart) the Clawdis gateway (Clawdis.app menubar, or `clawdis gateway`) and try again.";
if (code === "ECONNREFUSED") {
return new Error(
`Can't reach the clawd browser control server at ${url} (connection refused). ${hint}`,
);
}
if (code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT") {
return new Error(
`Can't reach the clawd browser control server at ${url} (timed out after ${timeoutMs}ms). ${hint}`,
);
}
const msg = String(err);
if (msg.toLowerCase().includes("abort")) {
return new Error(
`Can't reach the clawd browser control server at ${url} (timed out after ${timeoutMs}ms). ${hint}`,
);
}
return new Error(
`Can't reach the clawd browser control server at ${url}. ${hint} (${msg})`,
);
}
async function fetchJson<T>(
url: string,
init?: RequestInit & { timeoutMs?: number },
@@ -35,8 +81,14 @@ async function fetchJson<T>(
const timeoutMs = init?.timeoutMs ?? 5000;
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), timeoutMs);
const res = await fetch(url, { ...init, signal: ctrl.signal } as RequestInit);
clearTimeout(t);
let res: Response;
try {
res = await fetch(url, { ...init, signal: ctrl.signal } as RequestInit);
} catch (err) {
throw enhanceBrowserFetchError(url, err, timeoutMs);
} finally {
clearTimeout(t);
}
if (!res.ok) {
const text = await res.text().catch(() => "");
throw new Error(text ? `${res.status}: ${text}` : `HTTP ${res.status}`);