# Files changed · PR \#24356 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/24356/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[fix\(preview\): report navigation timeout accurately](https://git.cafe/pingdotgg/t3code/pull/24356.md)

Version: 20

Head commit: 29ae8e2f73c470653a3786934148fcab60125965

Base commit: 91ea0e18ebe2db81752bad7f0d4fa70a66d7562b

Comparison base: 91ea0e18ebe2db81752bad7f0d4fa70a66d7562b

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/server/src/mcp/PreviewAutomationBroker.ts

Status: modified; +14; −1

```diff
@@ -205,6 +205,14 @@
   }
 }
 
+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 @@
         ...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({
```

## apps/server/src/preview/ServerBrowser.test.ts

Status: modified; +31; −0

```diff
@@ -379,6 +379,37 @@
   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<PreviewAutomationStatus>({
+          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) =>
```

## apps/server/src/preview/ServerBrowser.ts

Status: modified; +4; −2

```diff
@@ -902,7 +902,9 @@
         (desktop || adopted) && page.url() === snapshot.navStatus.url
           ? page.reload(options)
           : page.goto(snapshot.navStatus.url, options)
+      ).then(constVoid, (cause: unknown) => {
+        throw ServerBrowserPage.toOperationError(cause, NAVIGATION_TIMEOUT_MS);
-      ).then(constVoid);
+      });
 
       if (tab.control.agentId !== null) tab.control.track(tab.initialNavigation);
       else void tab.initialNavigation.catch(constVoid);
@@ -1458,7 +1460,7 @@
           `Navigation to ${url} failed: ${ServerBrowserPage.parseNetError(message).description}`,
         );
       }
+      throw ServerBrowserPage.toOperationError(cause, timeout);
-      throw cause;
     });
   };
 
```

## apps/server/src/preview/ServerBrowserPage.ts

Status: modified; +9; −2

```diff
@@ -40,7 +40,10 @@
   }
 }
 
+export const toOperationError = (
+  cause: unknown,
-export const toOperationError = (cause: unknown): ServerBrowserOperationError => {
+  timeoutMs?: number,
+): ServerBrowserOperationError => {
   if (cause instanceof BrowserControlInterrupted)
     return new ServerBrowserOperationError(
       "PreviewAutomationControlInterruptedError",
@@ -51,7 +54,11 @@
   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,
+      timeoutMs === undefined ? undefined : { timeoutMs },
-    return new ServerBrowserOperationError("PreviewAutomationTimeoutError", firstLine);
+    );
   }
   if (
     /while parsing selector|Unknown engine|Unexpected token|strict mode violation/i.test(message)
```
