From 0d71c0e302ce750ca04d8807d4af6b31b99f28a9 Mon Sep 17 00:00:00 2001 From: Dario Piotrowicz Date: Thu, 24 Sep 2026 18:24:12 +0100 Subject: [PATCH] Fail early when `createTestHarness()` cannot start its proxy under Bun --- .changeset/fuzzy-bears-listen.md | 7 +++ .../src/__tests__/api/test-harness.test.ts | 43 +++++++++++++++++ .../src/__tests__/utils/is-bun.test.ts | 18 +++++++ .../src/api/startDevWorker/ProxyController.ts | 14 ++++-- .../wrangler/src/api/test-harness-runtime.ts | 47 +++++++++++++++++++ packages/wrangler/src/api/test-harness.ts | 9 ++++ packages/wrangler/src/utils/is-bun.ts | 8 ++++ .../src/utils/logPossibleBugMessage.ts | 3 +- .../templates/startDevWorker/ProxyWorker.ts | 5 ++ 9 files changed, 148 insertions(+), 6 deletions(-) create mode 100644 .changeset/fuzzy-bears-listen.md create mode 100644 packages/wrangler/src/__tests__/api/test-harness.test.ts create mode 100644 packages/wrangler/src/__tests__/utils/is-bun.test.ts create mode 100644 packages/wrangler/src/api/test-harness-runtime.ts create mode 100644 packages/wrangler/src/utils/is-bun.ts diff --git a/.changeset/fuzzy-bears-listen.md b/.changeset/fuzzy-bears-listen.md new file mode 100644 index 00000000000..da412f9b2a1 --- /dev/null +++ b/.changeset/fuzzy-bears-listen.md @@ -0,0 +1,7 @@ +--- +"wrangler": patch +--- + +Fail early when `createTestHarness()` cannot start its proxy under Bun + +When Bun does not deliver the proxy control request required by Miniflare, `createTestHarness()` now throws an actionable startup error. This prevents the process from remaining idle while every request to the test harness hangs indefinitely. diff --git a/packages/wrangler/src/__tests__/api/test-harness.test.ts b/packages/wrangler/src/__tests__/api/test-harness.test.ts new file mode 100644 index 00000000000..3118d796345 --- /dev/null +++ b/packages/wrangler/src/__tests__/api/test-harness.test.ts @@ -0,0 +1,43 @@ +import { describe, it, vi } from "vitest"; +import { waitForBunProxyMessages } from "../../api/test-harness-runtime"; + +describe("waitForBunProxyMessages", () => { + it("allows Bun when proxy messages are delivered", async ({ expect }) => { + await expect( + waitForBunProxyMessages(() => Promise.resolve(true), 1) + ).resolves.toBeUndefined(); + }); + + it("throws a UserError when Bun fails to deliver proxy messages", async ({ + expect, + }) => { + await expect( + waitForBunProxyMessages(() => Promise.resolve(false), 1) + ).rejects.toMatchObject({ + message: + "`createTestHarness()` could not start because Bun does not support the custom `fetch()` dispatcher required to deliver Miniflare's proxy control request. Run your tests using Node.js instead.", + telemetryMessage: "test harness bun proxy request failed", + }); + }); + + it("throws a UserError when Bun does not deliver proxy messages", async ({ + expect, + }) => { + vi.useFakeTimers(); + + try { + const rejection = expect( + waitForBunProxyMessages(() => new Promise(() => {}), 100) + ).rejects.toMatchObject({ + message: + "`createTestHarness()` could not start because Bun does not support the custom `fetch()` dispatcher required to deliver Miniflare's proxy control request. Run your tests using Node.js instead.", + telemetryMessage: "test harness bun proxy request failed", + }); + + await vi.advanceTimersByTimeAsync(100); + await rejection; + } finally { + vi.useRealTimers(); + } + }); +}); diff --git a/packages/wrangler/src/__tests__/utils/is-bun.test.ts b/packages/wrangler/src/__tests__/utils/is-bun.test.ts new file mode 100644 index 00000000000..6eea4df2ff5 --- /dev/null +++ b/packages/wrangler/src/__tests__/utils/is-bun.test.ts @@ -0,0 +1,18 @@ +import { describe, it } from "vitest"; +import { isBun } from "../../utils/is-bun"; + +describe("isBun", () => { + it("detects whether Bun is the current runtime", ({ expect }) => { + const originalBunVersion = process.versions.bun; + + try { + process.versions.bun = undefined; + expect(isBun()).toBe(false); + + process.versions.bun = "1.4.2"; + expect(isBun()).toBe(true); + } finally { + process.versions.bun = originalBunVersion; + } + }); +}); diff --git a/packages/wrangler/src/api/startDevWorker/ProxyController.ts b/packages/wrangler/src/api/startDevWorker/ProxyController.ts index d507f29bcbb..1e033409deb 100644 --- a/packages/wrangler/src/api/startDevWorker/ProxyController.ts +++ b/packages/wrangler/src/api/startDevWorker/ProxyController.ts @@ -328,18 +328,19 @@ export class ProxyController extends Controller { } runtimeMessageMutex = new Mutex(); + latestReloadCompleteMessage = Promise.resolve(true); async sendMessageToProxyWorker( message: ProxyWorkerIncomingRequestBody, retries = 3 - ): Promise { + ): Promise { if (this._torndown) { - return; + return false; } // Don't do any async work here. Enqueue the message with the mutex immediately. try { - await this.runtimeMessageMutex.runWith(async () => { + const response = await this.runtimeMessageMutex.runWith(async () => { const { proxyWorker } = await this.ready.promise; const ready = await proxyWorker.ready.catch(() => undefined); @@ -355,9 +356,11 @@ export class ProxyController extends Controller { } ); }); + + return response?.status === 204; } catch (cause) { if (this._torndown) { - return; + return false; } const error = castErrorCause(cause); @@ -370,6 +373,7 @@ export class ProxyController extends Controller { `Failed to send message to ProxyWorker: ${JSON.stringify(message)}`, error ); + return false; } } async sendMessageToInspectorProxyWorker( @@ -456,7 +460,7 @@ export class ProxyController extends Controller { this.latestConfig = data.config; this.latestBundle = data.bundle; - void this.sendMessageToProxyWorker({ + this.latestReloadCompleteMessage = this.sendMessageToProxyWorker({ type: "play", proxyData: data.proxyData, }); diff --git a/packages/wrangler/src/api/test-harness-runtime.ts b/packages/wrangler/src/api/test-harness-runtime.ts new file mode 100644 index 00000000000..a0cfd145678 --- /dev/null +++ b/packages/wrangler/src/api/test-harness-runtime.ts @@ -0,0 +1,47 @@ +import { UserError } from "@cloudflare/workers-utils"; + +// A working dispatcher delivers the ProxyWorker control request in milliseconds. +// Five seconds leaves ample startup headroom while turning Bun's ignored +// `dispatcher` option into an actionable error instead of an indefinite hang. +const BUN_PROXY_MESSAGE_TIMEOUT_MS = 5_000; + +function createBunProxyMessageError(): UserError { + return new UserError( + "`createTestHarness()` could not start because Bun does not support the custom `fetch()` dispatcher required to deliver Miniflare's proxy control request. Run your tests using Node.js instead.", + { telemetryMessage: "test harness bun proxy request failed" } + ); +} + +/** + * Waits for Bun to deliver the control message that unlocks the test harness + * proxy. Current Bun releases are affected by an upstream dispatcher issue and + * do not deliver this message. A possible future compatible release can resolve + * normally; until then, the timeout prevents requests from hanging indefinitely. + * + * @param waitForProxyMessages Callback that reports whether the latest proxy + * control message was delivered. + * @param timeoutMs Maximum time to wait when running under Bun. + * @returns A promise that resolves after the proxy control message is delivered. + * @throws {UserError} If Bun does not deliver the proxy control message. + */ +export async function waitForBunProxyMessages( + waitForProxyMessages: () => Promise, + timeoutMs = BUN_PROXY_MESSAGE_TIMEOUT_MS +): Promise { + let timeoutId: ReturnType | undefined; + try { + const delivered = await Promise.race([ + waitForProxyMessages(), + new Promise((_resolve, reject) => { + timeoutId = setTimeout(() => { + reject(createBunProxyMessageError()); + }, timeoutMs); + }), + ]); + if (!delivered) { + throw createBunProxyMessageError(); + } + } finally { + clearTimeout(timeoutId); + } +} diff --git a/packages/wrangler/src/api/test-harness.ts b/packages/wrangler/src/api/test-harness.ts index 16fcf4f3d55..d89b9e27b34 100644 --- a/packages/wrangler/src/api/test-harness.ts +++ b/packages/wrangler/src/api/test-harness.ts @@ -28,9 +28,11 @@ import { validateNodeCompatMode } from "../deployment-bundle/node-compat"; import { getDurableObjectClassNameToUseSQLiteMap } from "../dev/class-names-sqlite"; import { runWithLogLevel } from "../logger"; import { requireApiToken, requireAuth } from "../user"; +import { isBun } from "../utils/is-bun"; import { DevEnv } from "./startDevWorker/DevEnv"; import { MultiworkerRuntimeController } from "./startDevWorker/MultiworkerRuntimeController"; import { NoOpProxyController } from "./startDevWorker/NoOpProxyController"; +import { waitForBunProxyMessages } from "./test-harness-runtime"; import type { CfAccount } from "../dev/create-worker-preview"; import type { ErrorEvent } from "./startDevWorker/events"; import type { WranglerStartDevWorkerInput } from "./startDevWorker/types"; @@ -448,6 +450,8 @@ type DebugLog = { * const response = await server.fetch("/api/users"); * await server.close(); * ``` + * @throws {UserError} If Bun cannot deliver the proxy control request required + * to start the server. */ export function createTestHarness(options?: TestHarnessOptions): TestHarness { let initialOptions = options; @@ -680,6 +684,11 @@ export function createTestHarness(options?: TestHarnessOptions): TestHarness { waitForReloadComplete(session), updateConfig(session, inputs), ]); + if (isBun()) { + await waitForBunProxyMessages( + () => session.primaryDevEnv.proxy.latestReloadCompleteMessage + ); + } debugLog("startup - completed"); return session; } catch (error) { diff --git a/packages/wrangler/src/utils/is-bun.ts b/packages/wrangler/src/utils/is-bun.ts new file mode 100644 index 00000000000..c1c08bc6834 --- /dev/null +++ b/packages/wrangler/src/utils/is-bun.ts @@ -0,0 +1,8 @@ +/** + * Returns whether Wrangler is running in the Bun runtime. + * + * @returns Whether Wrangler is running in the Bun runtime. + */ +export function isBun(): boolean { + return !!process.versions.bun; +} diff --git a/packages/wrangler/src/utils/logPossibleBugMessage.ts b/packages/wrangler/src/utils/logPossibleBugMessage.ts index 718ba7b0d84..801bbb94ebe 100644 --- a/packages/wrangler/src/utils/logPossibleBugMessage.ts +++ b/packages/wrangler/src/utils/logPossibleBugMessage.ts @@ -1,12 +1,13 @@ import { logger } from "../logger"; import { updateCheck } from "../update-check"; import { fgGreenColor, resetColor } from "./constants"; +import { isBun } from "./is-bun"; /** * Write a message to the log that tells the user what they might do after we have reported an unexpected error. */ export async function logPossibleBugMessage() { - if (process.versions.bun) { + if (isBun()) { logger.warn( `Wrangler does not support the Bun runtime. Please try this command again using Node.js via \`npm\` or \`pnpm\`. Alternatively, make sure you're not passing the \`--bun\` flag when running \`bun run wrangler ...\`` ); diff --git a/packages/wrangler/templates/startDevWorker/ProxyWorker.ts b/packages/wrangler/templates/startDevWorker/ProxyWorker.ts index 87ca82c8941..5c9f55a210a 100644 --- a/packages/wrangler/templates/startDevWorker/ProxyWorker.ts +++ b/packages/wrangler/templates/startDevWorker/ProxyWorker.ts @@ -96,6 +96,11 @@ export class ProxyWorker implements DurableObject { .forEach((ws) => ws.send("reload")); break; + + default: + // Bun currently drops `cf.hostMetadata`, so acknowledge only control + // requests whose payload actually reached the ProxyWorker. + return new Response(null, { status: 400 }); } return new Response(null, { status: 204 });