Merge pull request #1055 from jiwangyihao/fix/browser-worker-startup-errors

fix(browser): surface tab worker startup errors
This commit is contained in:
Can Bölük
2026-05-14 04:47:31 +02:00
committed by GitHub
2 changed files with 104 additions and 10 deletions
@@ -30,6 +30,7 @@ import type {
interface WorkerHandle {
send(msg: WorkerInbound, transferList?: Transferable[]): void;
onMessage(handler: (msg: WorkerOutbound) => void): () => void;
onError(handler: (error: Error) => void): () => void;
terminate(): Promise<void>;
readonly mode: "worker" | "inline";
}
@@ -120,23 +121,14 @@ export async function acquireTab(
const initPayload = await buildInitPayload(browser, opts);
const worker = await spawnTabWorker();
const { promise, resolve, reject } = Promise.withResolvers<ReadyInfo>();
const unlisten = worker.onMessage(msg => {
if (msg.type === "ready") resolve(msg.info);
else if (msg.type === "init-failed") reject(errorFromPayload(msg.error));
else if (msg.type === "log") logWorkerMessage(msg);
});
let info: ReadyInfo;
try {
worker.send({ type: "init", payload: initPayload });
info = await raceWithTimeout(promise, opts.timeoutMs + GRACE_MS, "Timed out initializing browser tab worker");
info = await initializeTabWorker(worker, initPayload, opts.timeoutMs + GRACE_MS);
} catch (error) {
unlisten();
await worker.terminate().catch(() => undefined);
if (browser.refCount === 0) await releaseBrowser(browser, { kill: false });
throw error;
}
unlisten();
holdBrowser(browser);
const tab: TabSession = {
@@ -473,6 +465,17 @@ function wrapBunWorker(worker: Worker): WorkerHandle {
worker.addEventListener("message", wrap);
return () => worker.removeEventListener("message", wrap);
},
onError(handler) {
const onError = (event: ErrorEvent): void => handler(errorFromWorkerEvent(event));
const onMessageError = (event: MessageEvent): void =>
handler(new ToolError(`Tab worker message error: ${String(event.data)}`));
worker.addEventListener("error", onError);
worker.addEventListener("messageerror", onMessageError);
return () => {
worker.removeEventListener("error", onError);
worker.removeEventListener("messageerror", onMessageError);
};
},
async terminate() {
worker.terminate();
},
@@ -511,6 +514,44 @@ async function spawnInlineWorker(): Promise<WorkerHandle> {
hostListeners.add(handler);
return () => hostListeners.delete(handler);
},
onError: () => () => {},
async terminate() {},
};
}
async function initializeTabWorker(
worker: WorkerHandle,
payload: WorkerInitPayload,
timeoutMs: number,
): Promise<ReadyInfo> {
const { promise, resolve, reject } = Promise.withResolvers<ReadyInfo>();
const unlisten = worker.onMessage(msg => {
if (msg.type === "ready") resolve(msg.info);
else if (msg.type === "init-failed") reject(errorFromPayload(msg.error));
else if (msg.type === "log") logWorkerMessage(msg);
});
const unlistenError = worker.onError(error => {
reject(new ToolError(`Tab worker failed during startup: ${error.message}`));
});
try {
worker.send({ type: "init", payload });
return await raceWithTimeout(promise, timeoutMs, "Timed out initializing browser tab worker");
} finally {
unlisten();
unlistenError();
}
}
export function initializeTabWorkerForTest(
worker: WorkerHandle,
payload: WorkerInitPayload,
timeoutMs: number,
): Promise<ReadyInfo> {
return initializeTabWorker(worker, payload, timeoutMs);
}
function errorFromWorkerEvent(event: ErrorEvent): Error {
if (event.error instanceof Error) return event.error;
if (event.message) return new Error(event.message);
return new Error("Unknown tab worker error");
}
@@ -0,0 +1,53 @@
import { describe, expect, it } from "bun:test";
import type { ReadyInfo, WorkerInbound, WorkerOutbound } from "../../src/tools/browser/tab-protocol";
import { initializeTabWorkerForTest } from "../../src/tools/browser/tab-supervisor";
class FakeStartupWorker {
#errorHandlers = new Set<(error: Error) => void>();
#messageHandlers = new Set<(msg: WorkerOutbound) => void>();
readonly sent: WorkerInbound[] = [];
readonly mode = "worker" as const;
send(msg: WorkerInbound): void {
this.sent.push(msg);
}
onMessage(handler: (msg: WorkerOutbound) => void): () => void {
this.#messageHandlers.add(handler);
return () => this.#messageHandlers.delete(handler);
}
onError(handler: (error: Error) => void): () => void {
this.#errorHandlers.add(handler);
return () => this.#errorHandlers.delete(handler);
}
async terminate(): Promise<void> {}
emitReady(info: ReadyInfo): void {
for (const handler of this.#messageHandlers) handler({ type: "ready", info });
}
emitError(error: Error): void {
for (const handler of this.#errorHandlers) handler(error);
}
}
const initPayload = {
mode: "headless" as const,
browserWSEndpoint: "ws://127.0.0.1/devtools/browser/test",
safeDir: "/tmp/omp-puppeteer",
timeoutMs: 1_000,
};
describe("browser tab worker startup", () => {
it("surfaces worker startup errors instead of waiting for the generic init timeout", async () => {
const worker = new FakeStartupWorker();
const pending = initializeTabWorkerForTest(worker, initPayload, 1_000);
worker.emitError(new Error("Cannot find tab-worker-entry.ts"));
await expect(pending).rejects.toThrow("Tab worker failed during startup: Cannot find tab-worker-entry.ts");
expect(worker.sent).toEqual([{ type: "init", payload: initPayload }]);
});
});