Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/fuzzy-bears-listen.md
Original file line number Diff line number Diff line change
@@ -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.
43 changes: 43 additions & 0 deletions packages/wrangler/src/__tests__/api/test-harness.test.ts
Original file line number Diff line number Diff line change
@@ -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<boolean>(() => {}), 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();
}
});
});
18 changes: 18 additions & 0 deletions packages/wrangler/src/__tests__/utils/is-bun.test.ts
Original file line number Diff line number Diff line change
@@ -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;
}
});
});
14 changes: 9 additions & 5 deletions packages/wrangler/src/api/startDevWorker/ProxyController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,18 +328,19 @@ export class ProxyController extends Controller {
}

runtimeMessageMutex = new Mutex();
latestReloadCompleteMessage = Promise.resolve(true);
async sendMessageToProxyWorker(
message: ProxyWorkerIncomingRequestBody,
retries = 3
): Promise<void> {
): Promise<boolean> {
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);
Expand All @@ -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);
Expand All @@ -370,6 +373,7 @@ export class ProxyController extends Controller {
`Failed to send message to ProxyWorker: ${JSON.stringify(message)}`,
error
);
return false;
}
}
async sendMessageToInspectorProxyWorker(
Expand Down Expand Up @@ -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,
});
Expand Down
47 changes: 47 additions & 0 deletions packages/wrangler/src/api/test-harness-runtime.ts
Original file line number Diff line number Diff line change
@@ -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<boolean>,
timeoutMs = BUN_PROXY_MESSAGE_TIMEOUT_MS
): Promise<void> {
let timeoutId: ReturnType<typeof setTimeout> | undefined;
try {
const delivered = await Promise.race([
waitForProxyMessages(),
new Promise<never>((_resolve, reject) => {
timeoutId = setTimeout(() => {
reject(createBunProxyMessageError());
}, timeoutMs);
}),
]);
if (!delivered) {
throw createBunProxyMessageError();
}
} finally {
clearTimeout(timeoutId);
}
}
9 changes: 9 additions & 0 deletions packages/wrangler/src/api/test-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
8 changes: 8 additions & 0 deletions packages/wrangler/src/utils/is-bun.ts
Original file line number Diff line number Diff line change
@@ -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;
}
3 changes: 2 additions & 1 deletion packages/wrangler/src/utils/logPossibleBugMessage.ts
Original file line number Diff line number Diff line change
@@ -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 ...\``
);
Expand Down
5 changes: 5 additions & 0 deletions packages/wrangler/templates/startDevWorker/ProxyWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
Loading