fix(browser): avoid buffering discarded 429 bodies

This commit is contained in:
Altay
2026-03-11 00:13:03 +03:00
parent 92870d4ab9
commit 7d80e4f61d
3 changed files with 16 additions and 2 deletions

View File

@@ -77,6 +77,7 @@ Docs: https://docs.openclaw.ai
- Models/Alibaba Cloud Model Studio: wire `MODELSTUDIO_API_KEY` through shared env auth, implicit provider discovery, and shell-env fallback so onboarding works outside the wizard too. (#40634) Thanks @pomelo-nwu.
- ACP/sessions_spawn: implicitly stream `mode="run"` ACP spawns to parent only for eligible subagent orchestrator sessions (heartbeat `target: "last"` with a usable session-local route), restoring parent progress relays without thread binding. (#42404) Thanks @davidguttman.
- Sessions/reset model recompute: clear stale runtime model, context-token, and system-prompt metadata before session resets recompute the replacement session, so resets pick up current defaults and explicit overrides instead of reusing old runtime model state. (#41173) thanks @PonyX-lab.
- Browser/Browserbase 429 handling: surface stable no-retry rate-limit guidance without buffering discarded HTTP 429 response bodies from remote browser services. (#40491) thanks @mvanhorn.
## 2026.3.8

View File

@@ -139,9 +139,11 @@ describe("fetchBrowserJson loopback auth", () => {
});
it("surfaces 429 from HTTP URL as rate-limit error with no-retry hint", async () => {
const text = vi.fn(async () => "max concurrent sessions exceeded");
const cancel = vi.fn(async () => {});
vi.stubGlobal(
"fetch",
vi.fn(async () => new Response("max concurrent sessions exceeded", { status: 429 })),
vi.fn(async () => ({ ok: false, status: 429, text, body: { cancel } }) as Response),
);
const thrown = await fetchBrowserJson<{ ok: boolean }>("http://127.0.0.1:18888/").catch(
@@ -155,6 +157,8 @@ describe("fetchBrowserJson loopback auth", () => {
expect(thrown.message).toContain("Browser service rate limit reached");
expect(thrown.message).toContain("Do NOT retry the browser tool");
expect(thrown.message).not.toContain("max concurrent sessions exceeded");
expect(text).not.toHaveBeenCalled();
expect(cancel).toHaveBeenCalledOnce();
});
it("surfaces 429 from HTTP URL without body detail when empty", async () => {

View File

@@ -153,6 +153,14 @@ function appendBrowserToolModelHint(message: string): string {
return `${message} ${BROWSER_TOOL_MODEL_HINT}`;
}
async function discardResponseBody(res: Response): Promise<void> {
try {
await res.body?.cancel();
} catch {
// Best effort only; we're already returning a stable error message.
}
}
function enhanceDispatcherPathError(url: string, err: unknown): Error {
const msg = normalizeErrorMessage(err);
const suffix = `${resolveBrowserFetchOperatorHint(url)} ${BROWSER_TOOL_MODEL_HINT}`;
@@ -205,13 +213,14 @@ async function fetchHttpJson<T>(
try {
const res = await fetch(url, { ...init, signal: ctrl.signal });
if (!res.ok) {
const text = await res.text().catch(() => "");
if (isRateLimitStatus(res.status)) {
// Do not reflect upstream response text into the error surface (log/agent injection risk)
await discardResponseBody(res);
throw new BrowserServiceError(
`${resolveBrowserRateLimitMessage(url)} ${BROWSER_TOOL_MODEL_HINT}`,
);
}
const text = await res.text().catch(() => "");
throw new BrowserServiceError(text || `HTTP ${res.status}`);
}
return (await res.json()) as T;