mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-24 22:34:28 +00:00
fix(browser): scope 429 hints to Browserbase URLs
This commit is contained in:
@@ -3,7 +3,7 @@ import { isLoopbackHost } from "../gateway/net.js";
|
||||
import { rawDataToString } from "../infra/ws.js";
|
||||
import { getDirectAgentForCdp, withNoProxyForCdpUrl } from "./cdp-proxy-bypass.js";
|
||||
import { CDP_HTTP_REQUEST_TIMEOUT_MS, CDP_WS_HANDSHAKE_TIMEOUT_MS } from "./cdp-timeouts.js";
|
||||
import { BROWSER_RATE_LIMIT_MESSAGE } from "./client-fetch.js";
|
||||
import { resolveBrowserRateLimitMessage } from "./client-fetch.js";
|
||||
import { getChromeExtensionRelayAuthHeaders } from "./extension-relay.js";
|
||||
|
||||
export { isLoopbackHost };
|
||||
@@ -175,9 +175,7 @@ export async function fetchCdpChecked(
|
||||
if (!res.ok) {
|
||||
if (res.status === 429) {
|
||||
// Do not reflect upstream response text into the error surface (log/agent injection risk)
|
||||
throw new Error(
|
||||
`${BROWSER_RATE_LIMIT_MESSAGE} Do NOT retry - wait for the current session to complete, or upgrade your plan.`,
|
||||
);
|
||||
throw new Error(`${resolveBrowserRateLimitMessage(url)} Do NOT retry the browser tool.`);
|
||||
}
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { BrowserDispatchResponse } from "./routes/dispatcher.js";
|
||||
|
||||
function okDispatchResponse(): BrowserDispatchResponse {
|
||||
return { status: 200, body: { ok: true } };
|
||||
}
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
loadConfig: vi.fn(() => ({
|
||||
@@ -9,7 +14,7 @@ const mocks = vi.hoisted(() => ({
|
||||
},
|
||||
})),
|
||||
startBrowserControlServiceFromConfig: vi.fn(async () => ({ ok: true })),
|
||||
dispatch: vi.fn(async () => ({ status: 200, body: { ok: true } })),
|
||||
dispatch: vi.fn(async (): Promise<BrowserDispatchResponse> => okDispatchResponse()),
|
||||
}));
|
||||
|
||||
vi.mock("../config/config.js", async (importOriginal) => {
|
||||
@@ -57,7 +62,7 @@ describe("fetchBrowserJson loopback auth", () => {
|
||||
},
|
||||
});
|
||||
mocks.startBrowserControlServiceFromConfig.mockReset().mockResolvedValue({ ok: true });
|
||||
mocks.dispatch.mockReset().mockResolvedValue({ status: 200, body: { ok: true } });
|
||||
mocks.dispatch.mockReset().mockResolvedValue(okDispatchResponse());
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -147,13 +152,16 @@ describe("fetchBrowserJson loopback auth", () => {
|
||||
if (!(thrown instanceof Error)) {
|
||||
throw new Error(`Expected Error, got ${String(thrown)}`);
|
||||
}
|
||||
expect(thrown.message).toContain("rate limit reached");
|
||||
expect(thrown.message).toContain("Browser service rate limit reached");
|
||||
expect(thrown.message).toContain("Do NOT retry the browser tool");
|
||||
expect(thrown.message).toContain("max concurrent sessions exceeded");
|
||||
expect(thrown.message).not.toContain("max concurrent sessions exceeded");
|
||||
});
|
||||
|
||||
it("surfaces 429 from HTTP URL without body detail when empty", async () => {
|
||||
vi.stubGlobal("fetch", vi.fn(async () => new Response("", { status: 429 })));
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => new Response("", { status: 429 })),
|
||||
);
|
||||
|
||||
const thrown = await fetchBrowserJson<{ ok: boolean }>("http://127.0.0.1:18888/").catch(
|
||||
(err: unknown) => err,
|
||||
@@ -167,6 +175,25 @@ describe("fetchBrowserJson loopback auth", () => {
|
||||
expect(thrown.message).toContain("Do NOT retry the browser tool");
|
||||
});
|
||||
|
||||
it("keeps Browserbase-specific wording for Browserbase 429 responses", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => new Response("max concurrent sessions exceeded", { status: 429 })),
|
||||
);
|
||||
|
||||
const thrown = await fetchBrowserJson<{ ok: boolean }>(
|
||||
"https://connect.browserbase.com/session",
|
||||
).catch((err: unknown) => err);
|
||||
|
||||
expect(thrown).toBeInstanceOf(Error);
|
||||
if (!(thrown instanceof Error)) {
|
||||
throw new Error(`Expected Error, got ${String(thrown)}`);
|
||||
}
|
||||
expect(thrown.message).toContain("Browserbase rate limit reached");
|
||||
expect(thrown.message).toContain("upgrade your plan");
|
||||
expect(thrown.message).not.toContain("max concurrent sessions exceeded");
|
||||
});
|
||||
|
||||
it("non-429 errors still produce generic messages", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
@@ -197,9 +224,9 @@ describe("fetchBrowserJson loopback auth", () => {
|
||||
if (!(thrown instanceof Error)) {
|
||||
throw new Error(`Expected Error, got ${String(thrown)}`);
|
||||
}
|
||||
expect(thrown.message).toContain("rate limit reached");
|
||||
expect(thrown.message).toContain("Browser service rate limit reached");
|
||||
expect(thrown.message).toContain("Do NOT retry the browser tool");
|
||||
expect(thrown.message).toContain("too many sessions");
|
||||
expect(thrown.message).not.toContain("too many sessions");
|
||||
});
|
||||
|
||||
it("keeps absolute URL failures wrapped as reachability errors", async () => {
|
||||
|
||||
@@ -102,7 +102,11 @@ const BROWSER_TOOL_MODEL_HINT =
|
||||
"Do NOT retry the browser tool — it will keep failing. " +
|
||||
"Use an alternative approach or inform the user that the browser is currently unavailable.";
|
||||
|
||||
export const BROWSER_RATE_LIMIT_MESSAGE =
|
||||
const BROWSER_SERVICE_RATE_LIMIT_MESSAGE =
|
||||
"Browser service rate limit reached. " +
|
||||
"Wait for the current session to complete, or retry later.";
|
||||
|
||||
const BROWSERBASE_RATE_LIMIT_MESSAGE =
|
||||
"Browserbase rate limit reached (max concurrent sessions). " +
|
||||
"Wait for the current session to complete, or upgrade your plan.";
|
||||
|
||||
@@ -110,6 +114,24 @@ function isRateLimitStatus(status: number): boolean {
|
||||
return status === 429;
|
||||
}
|
||||
|
||||
function isBrowserbaseUrl(url: string): boolean {
|
||||
if (!isAbsoluteHttp(url)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const host = new URL(url).hostname.toLowerCase();
|
||||
return host === "browserbase.com" || host.endsWith(".browserbase.com");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveBrowserRateLimitMessage(url: string): string {
|
||||
return isBrowserbaseUrl(url)
|
||||
? BROWSERBASE_RATE_LIMIT_MESSAGE
|
||||
: BROWSER_SERVICE_RATE_LIMIT_MESSAGE;
|
||||
}
|
||||
|
||||
function resolveBrowserFetchOperatorHint(url: string): string {
|
||||
const isLocal = !isAbsoluteHttp(url);
|
||||
return isLocal
|
||||
@@ -186,7 +208,9 @@ async function fetchHttpJson<T>(
|
||||
const text = await res.text().catch(() => "");
|
||||
if (isRateLimitStatus(res.status)) {
|
||||
// Do not reflect upstream response text into the error surface (log/agent injection risk)
|
||||
throw new BrowserServiceError(`${BROWSER_RATE_LIMIT_MESSAGE} ${BROWSER_TOOL_MODEL_HINT}`);
|
||||
throw new BrowserServiceError(
|
||||
`${resolveBrowserRateLimitMessage(url)} ${BROWSER_TOOL_MODEL_HINT}`,
|
||||
);
|
||||
}
|
||||
throw new BrowserServiceError(text || `HTTP ${res.status}`);
|
||||
}
|
||||
@@ -283,7 +307,9 @@ export async function fetchBrowserJson<T>(
|
||||
if (result.status >= 400) {
|
||||
if (isRateLimitStatus(result.status)) {
|
||||
// Do not reflect upstream response text into the error surface (log/agent injection risk)
|
||||
throw new BrowserServiceError(`${BROWSER_RATE_LIMIT_MESSAGE} ${BROWSER_TOOL_MODEL_HINT}`);
|
||||
throw new BrowserServiceError(
|
||||
`${resolveBrowserRateLimitMessage(url)} ${BROWSER_TOOL_MODEL_HINT}`,
|
||||
);
|
||||
}
|
||||
const message =
|
||||
result.body && typeof result.body === "object" && "error" in result.body
|
||||
|
||||
Reference in New Issue
Block a user