-
-
Notifications
You must be signed in to change notification settings - Fork 80.7k
fix(browser): keep upload errors specific #101465
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import { beforeEach, describe, expect, it, vi } from "vitest"; | ||
|
|
||
| const clientFetchMocks = vi.hoisted(() => ({ | ||
| fetchBrowserJson: vi.fn(async (..._args: unknown[]) => ({ ok: true })), | ||
| })); | ||
|
|
||
| vi.mock("./client-fetch.js", () => clientFetchMocks); | ||
|
|
||
| import { browserArmDialog, browserArmFileChooser } from "./client-actions-core.js"; | ||
|
|
||
| function lastFetchCall(): { url: string; options: { body?: string; timeoutMs?: number } } { | ||
| const call = clientFetchMocks.fetchBrowserJson.mock.calls.at(-1); | ||
| if (!call) { | ||
| throw new Error("fetchBrowserJson was not called"); | ||
| } | ||
| return { | ||
| url: String(call[0]), | ||
| options: call[1] as { body?: string; timeoutMs?: number }, | ||
| }; | ||
| } | ||
|
|
||
| describe("browser hook client actions", () => { | ||
| beforeEach(() => vi.clearAllMocks()); | ||
|
|
||
| it("adds transport slack to an atomic file chooser upload", async () => { | ||
| await browserArmFileChooser(undefined, { | ||
| paths: ["/tmp/openclaw/uploads/report.pdf"], | ||
| ref: "e12", | ||
| targetId: "tab-1", | ||
| timeoutMs: 30_000, | ||
| profile: "openclaw", | ||
| }); | ||
|
|
||
| const call = lastFetchCall(); | ||
| expect(call.url).toBe("/hooks/file-chooser?profile=openclaw"); | ||
| expect(call.options.timeoutMs).toBe(35_000); | ||
| expect(JSON.parse(call.options.body ?? "{}")).toEqual({ | ||
| paths: ["/tmp/openclaw/uploads/report.pdf"], | ||
| ref: "e12", | ||
| targetId: "tab-1", | ||
| timeoutMs: 30_000, | ||
| }); | ||
| }); | ||
|
|
||
| it("preserves paths-only arming with the advertised 120 second default", async () => { | ||
| await browserArmFileChooser(undefined, { | ||
| paths: ["/tmp/openclaw/uploads/report.pdf"], | ||
| }); | ||
|
|
||
| const call = lastFetchCall(); | ||
| expect(call.url).toBe("/hooks/file-chooser"); | ||
| expect(call.options.timeoutMs).toBe(125_000); | ||
| expect(JSON.parse(call.options.body ?? "{}")).toEqual({ | ||
| paths: ["/tmp/openclaw/uploads/report.pdf"], | ||
| }); | ||
| }); | ||
|
|
||
| it("keeps dialog hook requests alive past their operation timeout", async () => { | ||
| await browserArmDialog(undefined, { accept: true, timeoutMs: 45_000 }); | ||
|
|
||
| const call = lastFetchCall(); | ||
| expect(call.url).toBe("/hooks/dialog"); | ||
| expect(call.options.timeoutMs).toBe(50_000); | ||
| expect(JSON.parse(call.options.body ?? "{}")).toEqual({ | ||
| accept: true, | ||
| timeoutMs: 45_000, | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,8 +38,7 @@ type BrowserActResponse = { | |
| downloads?: BrowserDownloadResult[]; | ||
| }; | ||
|
|
||
| const BROWSER_ACT_REQUEST_TIMEOUT_SLACK_MS = 5_000; | ||
| const BROWSER_DOWNLOAD_REQUEST_TIMEOUT_SLACK_MS = 5_000; | ||
| const BROWSER_REQUEST_TIMEOUT_SLACK_MS = 5_000; | ||
|
|
||
| type BrowserDownloadActionResult = BrowserActionTabResult & { download: BrowserDownloadResult }; | ||
|
|
||
|
|
@@ -52,24 +51,23 @@ function resolveBrowserActRequestTimeoutMs(req: BrowserActRequest): number { | |
| const candidateTimeouts = | ||
| explicitTimeout === undefined | ||
| ? [DEFAULT_BROWSER_ACTION_TIMEOUT_MS] | ||
| : [addTimerTimeoutGraceMs(explicitTimeout, BROWSER_ACT_REQUEST_TIMEOUT_SLACK_MS) ?? 1]; | ||
| : [addTimerTimeoutGraceMs(explicitTimeout, BROWSER_REQUEST_TIMEOUT_SLACK_MS) ?? 1]; | ||
| if (req.kind === "wait") { | ||
| const waitDuration = normalizePositiveTimeoutMs(req.timeMs); | ||
| if (waitDuration !== undefined) { | ||
| candidateTimeouts.push( | ||
| addTimerTimeoutGraceMs(waitDuration, BROWSER_ACT_REQUEST_TIMEOUT_SLACK_MS) ?? 1, | ||
| addTimerTimeoutGraceMs(waitDuration, BROWSER_REQUEST_TIMEOUT_SLACK_MS) ?? 1, | ||
| ); | ||
| } | ||
| } | ||
| return Math.max(...candidateTimeouts); | ||
| } | ||
|
|
||
| function resolveBrowserDownloadRequestTimeoutMs(timeoutMs: unknown): number { | ||
| const waitTimeoutMs = | ||
| function resolveBrowserOperationRequestTimeoutMs(timeoutMs: unknown): number { | ||
| const operationTimeoutMs = | ||
| normalizePositiveTimeoutMs(timeoutMs) ?? DEFAULT_BROWSER_DOWNLOAD_TIMEOUT_MS; | ||
| // Keep the HTTP client alive after the Playwright waiter expires so its | ||
| // timeout/error response reaches the caller instead of a transport abort. | ||
| return addTimerTimeoutGraceMs(waitTimeoutMs, BROWSER_DOWNLOAD_REQUEST_TIMEOUT_SLACK_MS) ?? 1; | ||
| // Let the browser operation report its own timeout/error before the client watchdog fires. | ||
| return addTimerTimeoutGraceMs(operationTimeoutMs, BROWSER_REQUEST_TIMEOUT_SLACK_MS) ?? 1; | ||
| } | ||
|
|
||
| async function postDownloadRequest( | ||
|
|
@@ -84,7 +82,7 @@ async function postDownloadRequest( | |
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify(body), | ||
| timeoutMs: resolveBrowserDownloadRequestTimeoutMs(timeoutMs), | ||
| timeoutMs: resolveBrowserOperationRequestTimeoutMs(timeoutMs), | ||
| }); | ||
| } | ||
|
|
||
|
|
@@ -129,7 +127,7 @@ export async function browserArmDialog( | |
| targetId: opts.targetId, | ||
| timeoutMs: opts.timeoutMs, | ||
| }), | ||
| timeoutMs: 20000, | ||
| timeoutMs: resolveBrowserOperationRequestTimeoutMs(opts.timeoutMs), | ||
| }); | ||
| } | ||
|
|
||
|
|
@@ -158,7 +156,7 @@ export async function browserArmFileChooser( | |
| targetId: opts.targetId, | ||
| timeoutMs: opts.timeoutMs, | ||
| }), | ||
| timeoutMs: 20000, | ||
| timeoutMs: resolveBrowserOperationRequestTimeoutMs(opts.timeoutMs), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| }); | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When an atomic upload includes
refand a smalltimeoutMs(for example 1000ms), this changed watchdog becomes onlytimeoutMs + 5000, but the server route applies that timeout only toarmFileUploadViaPlaywright; the subsequentclickViaPlaywrightinextensions/browser/src/browser/routes/agent.act.hooks.tsis called withouttimeoutMsand falls back to the 8000ms interaction default fromact-policy.ts. For a stale/hidden trigger ref, the client can still abort first and return the generic browser-control timeout this change is trying to avoid. Either include the trigger-click timeout/default in this outer timeout calculation or pass the upload timeout through to the trigger click.Useful? React with 👍 / 👎.