diff --git a/apps/server/src/mcp/PreviewAutomationBroker.ts b/apps/server/src/mcp/PreviewAutomationBroker.ts index c1ae21dfd3b4c..4e16f702b25c1 100644 --- a/apps/server/src/mcp/PreviewAutomationBroker.ts +++ b/apps/server/src/mcp/PreviewAutomationBroker.ts @@ -205,6 +205,14 @@ function remoteDetailKind(detail: unknown): RemoteDetailKind { } } +const readRemoteTimeoutMs = (detail: unknown): number | undefined => { + if (typeof detail !== "object" || detail === null || !("timeoutMs" in detail)) return undefined; + const timeoutMs = detail.timeoutMs; + return typeof timeoutMs === "number" && Number.isInteger(timeoutMs) && timeoutMs > 0 + ? timeoutMs + : undefined; +}; + const MAX_REASON_CHARS = 500; const classifyResponseError = ( @@ -253,11 +261,16 @@ const classifyResponseError = ( ...context, ...remoteDiagnostics, }); - case "PreviewAutomationTimeoutError": + case "PreviewAutomationTimeoutError": { + const remoteTimeoutMs = readRemoteTimeoutMs(error.detail); return new PreviewAutomationTimeoutError({ ...context, ...remoteDiagnostics, + ...(remoteTimeoutMs === undefined + ? {} + : { timeoutMs: Math.min(context.timeoutMs, remoteTimeoutMs) }), }); + } case "PreviewAutomationControlInterruptedError": { const reason = decodeControlReason(error.detail); return new PreviewAutomationControlInterruptedError({ diff --git a/apps/server/src/preview/ServerBrowser.test.ts b/apps/server/src/preview/ServerBrowser.test.ts index 2b33f3b370804..8ac47a279478e 100644 --- a/apps/server/src/preview/ServerBrowser.test.ts +++ b/apps/server/src/preview/ServerBrowser.test.ts @@ -379,6 +379,37 @@ beforeEach(() => { desktopConnections.length = 0; }); +it.live("preview_open reports its navigation timeout instead of the request budget", () => + Effect.scoped( + Effect.gen(function* () { + const { broker } = yield* ready; + initialNavigation = async () => { + throw Object.assign(new Error("page.goto: Timeout 15000ms exceeded."), { + name: "TimeoutError", + }); + }; + + const error = yield* broker + .invoke({ + scope, + operation: "open", + input: { + url: "http://localhost:1337/", + reuseExistingTab: false, + show: false, + }, + }) + .pipe(Effect.flip); + + expect(error).toMatchObject({ + _tag: "PreviewAutomationTimeoutError", + timeoutMs: 15_000, + }); + expect(error.message).toBe("Preview automation open timed out after 15000ms."); + }), + ).pipe(Effect.provide(layer)), +); + it.live.each(["desktop" as const, undefined])( "explicit desktop targets ($0 runtime) fail without creating a server tab", (runtime) => diff --git a/apps/server/src/preview/ServerBrowser.ts b/apps/server/src/preview/ServerBrowser.ts index 3544cd172a048..641c0eb41a40a 100644 --- a/apps/server/src/preview/ServerBrowser.ts +++ b/apps/server/src/preview/ServerBrowser.ts @@ -902,7 +902,9 @@ const make = Effect.gen(function* () { (desktop || adopted) && page.url() === snapshot.navStatus.url ? page.reload(options) : page.goto(snapshot.navStatus.url, options) - ).then(constVoid); + ).then(constVoid, (cause: unknown) => { + throw ServerBrowserPage.toOperationError(cause, NAVIGATION_TIMEOUT_MS); + }); if (tab.control.agentId !== null) tab.control.track(tab.initialNavigation); else void tab.initialNavigation.catch(constVoid); @@ -1458,7 +1460,7 @@ const make = Effect.gen(function* () { `Navigation to ${url} failed: ${ServerBrowserPage.parseNetError(message).description}`, ); } - throw cause; + throw ServerBrowserPage.toOperationError(cause, timeout); }); }; diff --git a/apps/server/src/preview/ServerBrowserPage.ts b/apps/server/src/preview/ServerBrowserPage.ts index 2355029258396..c6ca0e4a0f8f2 100644 --- a/apps/server/src/preview/ServerBrowserPage.ts +++ b/apps/server/src/preview/ServerBrowserPage.ts @@ -40,7 +40,10 @@ export class ServerBrowserOperationError extends Error { } } -export const toOperationError = (cause: unknown): ServerBrowserOperationError => { +export const toOperationError = ( + cause: unknown, + timeoutMs?: number, +): ServerBrowserOperationError => { if (cause instanceof BrowserControlInterrupted) return new ServerBrowserOperationError( "PreviewAutomationControlInterruptedError", @@ -51,7 +54,11 @@ export const toOperationError = (cause: unknown): ServerBrowserOperationError => const message = cause instanceof Error ? cause.message : String(cause); const firstLine = message.split("\n")[0] ?? message; if (cause instanceof Error && cause.name === "TimeoutError") { - return new ServerBrowserOperationError("PreviewAutomationTimeoutError", firstLine); + return new ServerBrowserOperationError( + "PreviewAutomationTimeoutError", + firstLine, + timeoutMs === undefined ? undefined : { timeoutMs }, + ); } if ( /while parsing selector|Unknown engine|Unexpected token|strict mode violation/i.test(message)