From 619ab210759e42dceb25078e61f7ab049c069fb3 Mon Sep 17 00:00:00 2001 From: emily-shen Date: Wed, 8 Jul 2026 12:09:33 +0100 Subject: [PATCH 1/6] remove miniflare's live reload --- .changeset/drop-live-reload-endpoint.md | 7 ++++ packages/miniflare/src/index.ts | 30 +++----------- packages/miniflare/src/plugins/core/index.ts | 31 -------------- .../miniflare/src/workers/core/constants.ts | 3 -- .../src/workers/core/entry.worker.ts | 41 ------------------- packages/miniflare/test/index.spec.ts | 17 +------- .../startDevWorker/LocalRuntimeController.ts | 3 -- .../src/api/startDevWorker/ProxyController.ts | 1 - packages/wrangler/src/dev/miniflare/index.ts | 2 - 9 files changed, 14 insertions(+), 121 deletions(-) create mode 100644 .changeset/drop-live-reload-endpoint.md diff --git a/.changeset/drop-live-reload-endpoint.md b/.changeset/drop-live-reload-endpoint.md new file mode 100644 index 0000000000..2112cc607c --- /dev/null +++ b/.changeset/drop-live-reload-endpoint.md @@ -0,0 +1,7 @@ +--- +"miniflare": major +--- + +Drop `/cdn-cgi/mf/reload` live reload endpoint and `liveReload` option + +The built-in live reload mechanism has been removed from Miniflare. This included a WebSocket endpoint at `/cdn-cgi/mf/reload`, the `liveReload` option, and the automatic injection of a live reload ``; - export const SCRIPT_CUSTOM_FETCH_SERVICE = `addEventListener("fetch", (event) => { const request = new Request(event.request); request.headers.set("${CoreHeaders.CUSTOM_FETCH_SERVICE}", ${CoreBindings.TEXT_CUSTOM_SERVICE}); @@ -1030,7 +1008,6 @@ export function getGlobalServices({ sharedOptions, allWorkerRoutes, fallbackWorkerName, - loopbackPort, tmpPath, log, proxyBindings, @@ -1141,14 +1118,6 @@ export function getGlobalServices({ data: encoder.encode(sharedOptions.unsafeProxySharedSecret), }); } - if (sharedOptions.liveReload) { - const liveReloadScript = LIVE_RELOAD_SCRIPT_TEMPLATE(loopbackPort); - serviceEntryBindings.push({ - name: CoreBindings.DATA_LIVE_RELOAD_SCRIPT, - data: encoder.encode(liveReloadScript), - }); - } - const services: Service[] = [ { name: SERVICE_LOOPBACK, diff --git a/packages/miniflare/src/workers/core/constants.ts b/packages/miniflare/src/workers/core/constants.ts index 4efb9455ea..a1c2c183a8 100644 --- a/packages/miniflare/src/workers/core/constants.ts +++ b/packages/miniflare/src/workers/core/constants.ts @@ -11,8 +11,6 @@ export const CorePaths = { EMAIL: "/cdn-cgi/handler/email", /** Handler path prefix for validation */ HANDLER_PREFIX: "/cdn-cgi/handler/", - /** Live reload WebSocket endpoint */ - LIVE_RELOAD: "/cdn-cgi/mf/reload", /** Local explorer UI and API */ EXPLORER: "/cdn-cgi/explorer", /** Legacy way to trigger scheduled event handlers */ @@ -68,7 +66,6 @@ export const CoreBindings = { JSON_CF_BLOB: "CF_BLOB", JSON_ROUTES: "MINIFLARE_ROUTES", JSON_LOG_LEVEL: "MINIFLARE_LOG_LEVEL", - DATA_LIVE_RELOAD_SCRIPT: "MINIFLARE_LIVE_RELOAD_SCRIPT", DURABLE_OBJECT_NAMESPACE_PROXY: "MINIFLARE_PROXY", DATA_PROXY_SECRET: "MINIFLARE_PROXY_SECRET", DATA_PROXY_SHARED_SECRET: "MINIFLARE_PROXY_SHARED_SECRET", diff --git a/packages/miniflare/src/workers/core/entry.worker.ts b/packages/miniflare/src/workers/core/entry.worker.ts index 013de035eb..240a28a807 100644 --- a/packages/miniflare/src/workers/core/entry.worker.ts +++ b/packages/miniflare/src/workers/core/entry.worker.ts @@ -21,7 +21,6 @@ type Env = { [CoreBindings.JSON_CF_BLOB]: IncomingRequestCfProperties; [CoreBindings.JSON_ROUTES]: WorkerRoute[]; [CoreBindings.JSON_LOG_LEVEL]: LogLevel; - [CoreBindings.DATA_LIVE_RELOAD_SCRIPT]?: ArrayBuffer; [CoreBindings.DURABLE_OBJECT_NAMESPACE_PROXY]: DurableObjectNamespace; [CoreBindings.DATA_PROXY_SHARED_SECRET]?: ArrayBuffer; [CoreBindings.TRIGGER_HANDLERS]: boolean; @@ -280,45 +279,6 @@ function maybePrettifyError(request: Request, response: Response, env: Env) { ); } -function maybeInjectLiveReload( - response: Response, - env: Env, - ctx: ExecutionContext -) { - const liveReloadScript = env[CoreBindings.DATA_LIVE_RELOAD_SCRIPT]; - if ( - liveReloadScript === undefined || - !response.headers.get("Content-Type")?.toLowerCase().includes("text/html") - ) { - return response; - } - - const headers = new Headers(response.headers); - const contentLength = parseInt(headers.get("content-length") ?? "NaN"); - if (!isNaN(contentLength)) { - headers.set( - "content-length", - String(contentLength + liveReloadScript.byteLength) - ); - } - - const { readable, writable } = new IdentityTransformStream(); - ctx.waitUntil( - (async () => { - await response.body?.pipeTo(writable, { preventClose: true }); - const writer = writable.getWriter(); - await writer.write(liveReloadScript); - await writer.close(); - })() - ); - - return new Response(readable, { - status: response.status, - statusText: response.statusText, - headers, - }); -} - const acceptEncodingElement = /^(?[a-z]+|\*)(?:\s*;\s*q=(?\d+(?:.\d+)?))?$/; interface AcceptedEncoding { @@ -619,7 +579,6 @@ export default >{ if (!disablePrettyErrorPage) { response = await maybePrettifyError(request, response, env); } - response = maybeInjectLiveReload(response, env, ctx); response = ensureAcceptableEncoding(clientAcceptEncoding, response); if (env[CoreBindings.LOG_REQUESTS]) { response = maybeLogRequest(request, response, env, ctx, startTime); diff --git a/packages/miniflare/test/index.spec.ts b/packages/miniflare/test/index.spec.ts index a3eaaad9ea..cfd37413fe 100644 --- a/packages/miniflare/test/index.spec.ts +++ b/packages/miniflare/test/index.spec.ts @@ -208,13 +208,9 @@ test("Miniflare: ready returns copy of entry URL", async ({ expect }) => { }); test("Miniflare: setOptions: can update host/port", async ({ expect }) => { - // Extract loopback port from injected live reload script - const loopbackPortRegexp = /\/\/ Miniflare Live Reload.+url\.port = (\d+)/s; - const opts: MiniflareOptions = { port: 0, inspectorPort: 0, - liveReload: true, script: `addEventListener("fetch", (event) => { event.respondWith(new Response("

👋

", { headers: { "Content-Type": "text/html;charset=utf-8" } @@ -227,9 +223,7 @@ test("Miniflare: setOptions: can update host/port", async ({ expect }) => { async function getState() { const url = await mf.ready; const inspectorUrl = await mf.getInspectorURL(); - const res = await mf.dispatchFetch("http://localhost"); - const loopbackPort = loopbackPortRegexp.exec(await res.text())?.[1]; - return { url, inspectorUrl, loopbackPort }; + return { url, inspectorUrl }; } const state1 = await getState(); @@ -243,19 +237,12 @@ test("Miniflare: setOptions: can update host/port", async ({ expect }) => { expect(state1.inspectorUrl.port).not.toBe("0"); expect(state1.inspectorUrl.port).toBe(state2.inspectorUrl.port); - // Make sure updating the host restarted the loopback server - expect(state1.loopbackPort).toBeDefined(); - expect(state2.loopbackPort).toBeDefined(); - expect(state1.loopbackPort).not.toBe(state2.loopbackPort); - - // Make sure setting port to `undefined` always gives a new port, but keeps - // existing loopback server + // Make sure setting port to `undefined` always gives a new port opts.port = undefined; await mf.setOptions(opts); const state3 = await getState(); expect(state3.url.port).not.toBe("0"); expect(state1.url.port).not.toBe(state3.url.port); - expect(state2.loopbackPort).toBe(state3.loopbackPort); }); const interfaces = os.networkInterfaces(); diff --git a/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts b/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts index 053b052ff1..a9525d3ed3 100644 --- a/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts +++ b/packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts @@ -193,7 +193,6 @@ export async function convertToConfigBundle( inspectorHost: event.config.dev.inspector?.hostname, }), localPersistencePath: event.config.dev.persist, - liveReload: event.config.dev?.liveReload ?? false, crons, routes: event.config.dev.routeRequestsByRoutes ? routes : undefined, queueConsumers, @@ -390,8 +389,6 @@ export class LocalRuntimeController extends RuntimeController { }); } ); - options.liveReload = false; // TODO: set in buildMiniflareOptions once old code path is removed - // Bail out if a newer bundle arrived while we were building // miniflare options — avoid a redundant local server reload. if (id !== this.#currentBundleId) { diff --git a/packages/wrangler/src/api/startDevWorker/ProxyController.ts b/packages/wrangler/src/api/startDevWorker/ProxyController.ts index 5296e05669..24f78259cf 100644 --- a/packages/wrangler/src/api/startDevWorker/ProxyController.ts +++ b/packages/wrangler/src/api/startDevWorker/ProxyController.ts @@ -133,7 +133,6 @@ export class ProxyController extends Controller { this.localServerReady.promise ), handleStructuredLogs, - liveReload: false, }; if (this.inspectorEnabled) { diff --git a/packages/wrangler/src/dev/miniflare/index.ts b/packages/wrangler/src/dev/miniflare/index.ts index 1ce6645813..c96a715e28 100644 --- a/packages/wrangler/src/dev/miniflare/index.ts +++ b/packages/wrangler/src/dev/miniflare/index.ts @@ -85,7 +85,6 @@ export interface ConfigBundle { inspectorPort: number | undefined; inspectorHost: string | undefined; localPersistencePath: string | false; - liveReload: boolean; crons: Config["triggers"]["crons"]; routes: string[] | undefined; queueConsumers: Config["queues"]["consumers"]; @@ -1150,7 +1149,6 @@ export async function buildMiniflareOptions( publicUrl: config.publicUrl, inspectorPort: config.inspect ? config.inspectorPort : undefined, inspectorHost: config.inspect ? config.inspectorHost : undefined, - liveReload: config.liveReload, upstream, unsafeDevRegistryPath: config.devRegistry, unsafeHandleDevRegistryUpdate: onDevRegistryUpdate, From 717450a5c42bd9c87e6da159b371ef6d7f6f2ed4 Mon Sep 17 00:00:00 2001 From: emily-shen Date: Wed, 8 Jul 2026 15:48:58 +0100 Subject: [PATCH 2/6] update paths in miniflare --- .changeset/remap-local-testing-paths.md | 17 ++++++++++ .../local-explorer-ui/src/__e2e__/utils.ts | 29 +++++++++------- .../src/__tests__/utils/agent-prompt.test.ts | 21 +++++------- .../src/components/Sidebar.tsx | 3 +- packages/local-explorer-ui/src/constants.ts | 9 +++++ .../src/routes/r2/$bucketName/index.tsx | 3 +- .../src/routes/r2/$bucketName/object.$.tsx | 3 +- packages/local-explorer-ui/tsconfig.e2e.json | 3 +- packages/local-explorer-ui/vite.config.ts | 10 +++--- packages/miniflare/scripts/filter-openapi.ts | 2 +- packages/miniflare/src/index.ts | 1 - packages/miniflare/src/plugins/core/index.ts | 5 ++- .../src/workers/assets/rpc-proxy.worker.ts | 2 +- .../miniflare/src/workers/core/constants.ts | 29 ++++++++-------- .../workers/core/dev-registry-proxy.worker.ts | 3 +- .../src/workers/core/entry.worker.ts | 33 +++++-------------- .../src/workers/images/images.worker.ts | 2 +- .../local-explorer/generated/types.gen.ts | 2 +- .../workers/local-explorer/openapi.local.json | 2 +- .../src/workers/local-explorer/route-names.ts | 4 +-- .../src/workers/stream/binding.worker.ts | 2 +- .../miniflare/src/workers/stream/schemas.ts | 4 ++- packages/miniflare/test/index.spec.ts | 28 ++++++++-------- .../test/plugins/email/index.spec.ts | 26 +++++++-------- .../test/plugins/images/index.spec.ts | 8 ++--- .../test/plugins/local-explorer/index.spec.ts | 32 +++++++++++------- .../plugins/local-explorer/telemetry.spec.ts | 12 ++++--- .../test/plugins/stream/index.spec.ts | 12 +++---- 28 files changed, 167 insertions(+), 140 deletions(-) create mode 100644 .changeset/remap-local-testing-paths.md create mode 100644 packages/local-explorer-ui/src/constants.ts diff --git a/.changeset/remap-local-testing-paths.md b/.changeset/remap-local-testing-paths.md new file mode 100644 index 0000000000..6c1bc45bc7 --- /dev/null +++ b/.changeset/remap-local-testing-paths.md @@ -0,0 +1,17 @@ +--- +"miniflare": major +--- + +Remap local testing paths to avoid collision with production `/cdn-cgi` routes + +All miniflare-internal endpoints have moved to more consistent paths. Paths that need to remain reachable over tunnels now live outside `/cdn-cgi/`: + +- `/cdn-cgi/platform-proxy` → `/cdn-cgi/local/platform-proxy` +- `/cdn-cgi/handler/scheduled` → `/cdn-cgi/local/scheduled` +- `/cdn-cgi/handler/email` → `/cdn-cgi/local/email` +- `/cdn-cgi/explorer/*` → `/cdn-cgi/local/explorer/*` +- `/cdn-cgi/mf/scheduled` → removed (was already deprecated) +- `/cdn-cgi/mf/stream/*` → `/__cf_local/stream/*` +- `/cdn-cgi/mf/imagedelivery/*` → `/__cf_local/imagedelivery/*` + +Wrangler and the Vite plugin will add transparent path rewrites so the old paths continue to work for users of those tools. diff --git a/packages/local-explorer-ui/src/__e2e__/utils.ts b/packages/local-explorer-ui/src/__e2e__/utils.ts index 0ed71ef8d4..79ecf82399 100644 --- a/packages/local-explorer-ui/src/__e2e__/utils.ts +++ b/packages/local-explorer-ui/src/__e2e__/utils.ts @@ -1,6 +1,10 @@ import { readFile, unlink } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { + LOCAL_EXPLORER_API_PATH, + LOCAL_EXPLORER_BASE_PATH, +} from "../constants"; import { page, viteUrl, workerUrl } from "./setup"; export { page, viteUrl }; @@ -57,7 +61,7 @@ export async function seedWorkflow(workflowName: string): Promise<{ const workflowId = `e2e-test-${Date.now()}`; await fetch( - `${workerUrl}/cdn-cgi/explorer/api/workflows/${workflowName}/instances`, + `${workerUrl}${LOCAL_EXPLORER_API_PATH}/workflows/${workflowName}/instances`, { body: JSON.stringify({ id: workflowId, @@ -79,9 +83,12 @@ export async function seedWorkflow(workflowName: string): Promise<{ * Delete all instances of a workflow via the explorer API. */ export async function cleanupWorkflow(workflowName: string): Promise { - await fetch(`${workerUrl}/cdn-cgi/explorer/api/workflows/${workflowName}`, { - method: "DELETE", - }); + await fetch( + `${workerUrl}${LOCAL_EXPLORER_API_PATH}/workflows/${workflowName}`, + { + method: "DELETE", + } + ); } /** @@ -96,7 +103,7 @@ const WAIT_OPTIONS = { * Navigate to a KV namespace. */ export async function navigateToKV(namespaceId: string): Promise { - await navigateTo(`/cdn-cgi/explorer/kv/${namespaceId}`); + await navigateTo(`${LOCAL_EXPLORER_BASE_PATH}/kv/${namespaceId}`); await waitForPageLoad(); } @@ -104,7 +111,7 @@ export async function navigateToKV(namespaceId: string): Promise { * Navigate to an R2 bucket. */ export async function navigateToR2Bucket(bucketName: string): Promise { - await navigateTo(`/cdn-cgi/explorer/r2/${bucketName}`); + await navigateTo(`${LOCAL_EXPLORER_BASE_PATH}/r2/${bucketName}`); await waitForPageLoad(); } @@ -116,7 +123,7 @@ export async function navigateToR2Object( objectKey: string ): Promise { await navigateTo( - `/cdn-cgi/explorer/r2/${bucketName}/object/${encodeURIComponent(objectKey)}` + `${LOCAL_EXPLORER_BASE_PATH}/r2/${bucketName}/object/${encodeURIComponent(objectKey)}` ); await waitForPageLoad(); } @@ -128,7 +135,7 @@ export async function navigateToD1( databaseId: string, table?: string ): Promise { - let path = `/cdn-cgi/explorer/d1/${databaseId}`; + let path = `${LOCAL_EXPLORER_BASE_PATH}/d1/${databaseId}`; if (table) { path += `?table=${encodeURIComponent(table)}`; } @@ -141,7 +148,7 @@ export async function navigateToD1( * Navigate to a Durable Object class. */ export async function navigateToDOClass(className: string): Promise { - await navigateTo(`/cdn-cgi/explorer/do/${className}`); + await navigateTo(`${LOCAL_EXPLORER_BASE_PATH}/do/${className}`); await waitForPageLoad(); } @@ -149,7 +156,7 @@ export async function navigateToDOClass(className: string): Promise { * Navigate to a Workflow instances list page. */ export async function navigateToWorkflow(workflowName: string): Promise { - await navigateTo(`/cdn-cgi/explorer/workflows/${workflowName}`); + await navigateTo(`${LOCAL_EXPLORER_BASE_PATH}/workflows/${workflowName}`); await waitForPageLoad(); } @@ -164,7 +171,7 @@ export async function navigateToDOObject( objectId: string, table?: string ): Promise { - let path = `/cdn-cgi/explorer/do/${className}/${objectId}`; + let path = `${LOCAL_EXPLORER_BASE_PATH}/do/${className}/${objectId}`; if (table) { path += `?table=${encodeURIComponent(table)}`; } diff --git a/packages/local-explorer-ui/src/__tests__/utils/agent-prompt.test.ts b/packages/local-explorer-ui/src/__tests__/utils/agent-prompt.test.ts index c150d49474..becd35108f 100644 --- a/packages/local-explorer-ui/src/__tests__/utils/agent-prompt.test.ts +++ b/packages/local-explorer-ui/src/__tests__/utils/agent-prompt.test.ts @@ -1,30 +1,27 @@ import { describe, test, vi } from "vitest"; +import { LOCAL_EXPLORER_API_PATH } from "../../constants"; import { copyTextToClipboard, createLocalExplorerPrompt, getLocalExplorerApiEndpoint, } from "../../utils/agent-prompt"; +const TEST_ORIGIN = "http://localhost:8787"; +const TEST_API_ENDPOINT = `${TEST_ORIGIN}${LOCAL_EXPLORER_API_PATH}`; + describe("llm-prompt utils", () => { test("builds api endpoint from origin and api path", ({ expect }) => { expect( - getLocalExplorerApiEndpoint( - "http://localhost:8787", - "/cdn-cgi/explorer/api" - ) - ).toBe("http://localhost:8787/cdn-cgi/explorer/api"); + getLocalExplorerApiEndpoint(TEST_ORIGIN, LOCAL_EXPLORER_API_PATH) + ).toBe(TEST_API_ENDPOINT); }); test("generates prompt text with resolved api endpoint", ({ expect }) => { - const prompt = createLocalExplorerPrompt( - "http://localhost:8787/cdn-cgi/explorer/api" - ); + const prompt = createLocalExplorerPrompt(TEST_API_ENDPOINT); + expect(prompt).toContain(`API endpoint: ${TEST_API_ENDPOINT}`); expect(prompt).toContain( - "API endpoint: http://localhost:8787/cdn-cgi/explorer/api" - ); - expect(prompt).toContain( - "Fetch the OpenAPI schema from http://localhost:8787/cdn-cgi/explorer/api" + `Fetch the OpenAPI schema from ${TEST_API_ENDPOINT}` ); }); diff --git a/packages/local-explorer-ui/src/components/Sidebar.tsx b/packages/local-explorer-ui/src/components/Sidebar.tsx index db075a3aae..7dcbdda119 100644 --- a/packages/local-explorer-ui/src/components/Sidebar.tsx +++ b/packages/local-explorer-ui/src/components/Sidebar.tsx @@ -13,6 +13,7 @@ import DOIcon from "../assets/icons/durable-objects.svg?react"; import KVIcon from "../assets/icons/kv.svg?react"; import R2Icon from "../assets/icons/r2.svg?react"; import WorkflowsIcon from "../assets/icons/workflows.svg?react"; +import { LOCAL_EXPLORER_BASE_PATH } from "../constants"; import { loadGroupState, saveGroupState } from "../utils/sidebar-state"; import { getNextThemeMode } from "../utils/theme-state"; import { SidebarGroupPopup } from "./SidebarGroupPopup"; @@ -198,7 +199,7 @@ export function AppSidebar({
(null); function handleDownload(): void { - const downloadUrl = `/cdn-cgi/explorer/api/r2/buckets/${encodeURIComponent(params.bucketName)}/objects/${encodeURIComponent(loaderData.objectKey)}`; + const downloadUrl = `${LOCAL_EXPLORER_API_PATH}/r2/buckets/${encodeURIComponent(params.bucketName)}/objects/${encodeURIComponent(loaderData.objectKey)}`; const link = document.createElement("a"); link.href = downloadUrl; link.download = loaderData.objectKey.split("/").pop() || "download"; diff --git a/packages/local-explorer-ui/tsconfig.e2e.json b/packages/local-explorer-ui/tsconfig.e2e.json index ba1ed01778..b1163aaf70 100644 --- a/packages/local-explorer-ui/tsconfig.e2e.json +++ b/packages/local-explorer-ui/tsconfig.e2e.json @@ -10,6 +10,7 @@ "src/__e2e__", "src/__e2e__/global-setup.ts", "src/__e2e__/setup.ts", - "../../fixtures/shared/src/run-wrangler-long-lived.ts" + "../../fixtures/shared/src/run-wrangler-long-lived.ts", + "src/constants.ts" ] } diff --git a/packages/local-explorer-ui/vite.config.ts b/packages/local-explorer-ui/vite.config.ts index 0e6fef0743..c75e8d79e9 100644 --- a/packages/local-explorer-ui/vite.config.ts +++ b/packages/local-explorer-ui/vite.config.ts @@ -3,12 +3,10 @@ import { tanstackRouter } from "@tanstack/router-plugin/vite"; import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; import svgr from "vite-plugin-svgr"; - -// Canonical definitions in packages/miniflare/src/plugins/core/constants.ts -// Cannot import from miniflare directly due to circular dependency -// (miniflare depends on @cloudflare/local-explorer-ui) -const LOCAL_EXPLORER_BASE_PATH = "/cdn-cgi/explorer"; -const LOCAL_EXPLORER_API_PATH = `${LOCAL_EXPLORER_BASE_PATH}/api`; +import { + LOCAL_EXPLORER_API_PATH, + LOCAL_EXPLORER_BASE_PATH, +} from "./src/constants"; export default defineConfig({ plugins: [ diff --git a/packages/miniflare/scripts/filter-openapi.ts b/packages/miniflare/scripts/filter-openapi.ts index c0085e455c..38e0398150 100644 --- a/packages/miniflare/scripts/filter-openapi.ts +++ b/packages/miniflare/scripts/filter-openapi.ts @@ -39,7 +39,7 @@ const LOCAL_EXPLORER_INFO = { const LOCAL_EXPLORER_SERVERS = [ { description: "Local Explorer", - url: "/cdn-cgi/explorer/api", + url: "/cdn-cgi/local/explorer/api", }, ]; diff --git a/packages/miniflare/src/index.ts b/packages/miniflare/src/index.ts index c03df5af22..c4ff3cf96f 100644 --- a/packages/miniflare/src/index.ts +++ b/packages/miniflare/src/index.ts @@ -2341,7 +2341,6 @@ export class Miniflare { ) ? `${RPC_PROXY_SERVICE_NAME}:${this.#workerOpts[0].core.name}` : getUserServiceName(this.#workerOpts[0].core.name), - loopbackPort, tmpPath: this.#tmpPath, log: this.#log, proxyBindings, diff --git a/packages/miniflare/src/plugins/core/index.ts b/packages/miniflare/src/plugins/core/index.ts index 8d5ae1f90a..2aee4e846f 100644 --- a/packages/miniflare/src/plugins/core/index.ts +++ b/packages/miniflare/src/plugins/core/index.ts @@ -308,14 +308,14 @@ export const CoreSharedOptionsSchema = z unsafeModuleFallbackService: CustomFetchServiceSchema.optional(), // Keep blobs when deleting/overwriting keys, required for stacked storage unsafeStickyBlobs: z.boolean().optional(), - // Enable directly triggering user Worker handlers with paths like `/cdn-cgi/handler/scheduled` + // Enable directly triggering user Worker handlers with paths like `/cdn-cgi/local/scheduled` unsafeTriggerHandlers: z.boolean().optional(), // Extra environment variables to set on the spawned `workerd` subprocess. // Merged on top of `process.env` and Miniflare's own defaults // (e.g. `TZ=UTC`, `FORCE_COLOR`), so callers can override those defaults // (for example, to test timezone-dependent behaviour). unsafeRuntimeEnv: z.record(z.string()).optional(), - // Enable the local explorer at /cdn-cgi/explorer + // Enable the local explorer at /cdn-cgi/local/explorer unsafeLocalExplorer: z.boolean().optional(), // Enable logging requests logRequests: z.boolean().default(true), @@ -992,7 +992,6 @@ export interface GlobalServicesOptions { sharedOptions: z.infer; allWorkerRoutes: Map; fallbackWorkerName: string | undefined; - loopbackPort: number; tmpPath: string; log: Log; /** All user workerd-native bindings, used for Miniflare's magic proxy and the local explorer worker */ diff --git a/packages/miniflare/src/workers/assets/rpc-proxy.worker.ts b/packages/miniflare/src/workers/assets/rpc-proxy.worker.ts index 8ed03b6009..e2d36a7744 100644 --- a/packages/miniflare/src/workers/assets/rpc-proxy.worker.ts +++ b/packages/miniflare/src/workers/assets/rpc-proxy.worker.ts @@ -30,7 +30,7 @@ export default class RPCProxyWorker extends WorkerEntrypoint { // any scheduled logic; it just dispatches a real scheduled event to the user // worker via the Fetcher built-in, then propagates the user worker's noRetry // decision back onto this controller so the outcome surfaces correctly to - // the caller (e.g. the entry worker's `/cdn-cgi/handler/scheduled` handler). + // the caller (e.g. the entry worker's `/cdn-cgi/local/scheduled` handler). async scheduled(controller: ScheduledController) { const result = await this.env.USER_WORKER.scheduled?.({ cron: controller.cron, diff --git a/packages/miniflare/src/workers/core/constants.ts b/packages/miniflare/src/workers/core/constants.ts index a1c2c183a8..1cfb0b8933 100644 --- a/packages/miniflare/src/workers/core/constants.ts +++ b/packages/miniflare/src/workers/core/constants.ts @@ -1,24 +1,25 @@ /** - * Reserved `/cdn-cgi/` paths for internal Miniflare endpoints. - * These paths are reserved by Cloudflare's network and won't conflict with user routes. + * Reserved paths for internal Miniflare endpoints. + * + * Paths under `/cdn-cgi/local/` are reserved by Cloudflare's network + * and won't conflict with user routes. Paths under `/__cf_local/` live + * outside `/cdn-cgi/` so they remain reachable over tunnels. */ export const CorePaths = { /** Magic proxy used by getPlatformProxy */ - PLATFORM_PROXY: "/cdn-cgi/platform-proxy", + PLATFORM_PROXY: "/cdn-cgi/local/platform-proxy", /** Trigger scheduled event handlers */ - SCHEDULED: "/cdn-cgi/handler/scheduled", + SCHEDULED: "/cdn-cgi/local/scheduled", /** Trigger email event handlers */ - EMAIL: "/cdn-cgi/handler/email", - /** Handler path prefix for validation */ - HANDLER_PREFIX: "/cdn-cgi/handler/", + EMAIL: "/cdn-cgi/local/email", + /** Local endpoint prefix for catch-all validation */ + LOCAL_PREFIX: "/cdn-cgi/local/", /** Local explorer UI and API */ - EXPLORER: "/cdn-cgi/explorer", - /** Legacy way to trigger scheduled event handlers */ - LEGACY_SCHEDULED: "/cdn-cgi/mf/scheduled", - /** Stream video serving endpoint */ - STREAM_VIDEO: "/cdn-cgi/mf/stream", - /** Local image delivery endpoint for serving hosted images */ - IMAGE_DELIVERY: "/cdn-cgi/mf/imagedelivery", + EXPLORER: "/cdn-cgi/local/explorer", + /** Stream video serving endpoint (outside /cdn-cgi/ for tunnel access) */ + STREAM_VIDEO: "/__cf_local/stream", + /** Local image delivery endpoint (outside /cdn-cgi/ for tunnel access) */ + IMAGE_DELIVERY: "/__cf_local/imagedelivery", /** Public R2 bucket object serving endpoint */ R2_PUBLIC: "/cdn-cgi/local/r2/public", } as const; diff --git a/packages/miniflare/src/workers/core/dev-registry-proxy.worker.ts b/packages/miniflare/src/workers/core/dev-registry-proxy.worker.ts index 392f46bd9e..8fc05d10d8 100644 --- a/packages/miniflare/src/workers/core/dev-registry-proxy.worker.ts +++ b/packages/miniflare/src/workers/core/dev-registry-proxy.worker.ts @@ -1,5 +1,6 @@ import { WorkerEntrypoint } from "cloudflare:workers"; import { getQueueServiceName, HEADER_QUEUE_NAME } from "../queues/constants"; +import { CorePaths } from "./constants"; import { findQueueConsumer, resolveTarget, @@ -145,7 +146,7 @@ export class ExternalServiceProxy extends WorkerEntrypoint { params.set("time", String(controller.scheduledTime)); } const response = await this._entryFetcher.fetch( - new Request(`http://localhost/cdn-cgi/handler/scheduled?${params}`, { + new Request(`http://localhost${CorePaths.SCHEDULED}?${params}`, { headers: { "MF-Route-Override": this.ctx.props.service }, }) ); diff --git a/packages/miniflare/src/workers/core/entry.worker.ts b/packages/miniflare/src/workers/core/entry.worker.ts index 240a28a807..bb3bb8a8f2 100644 --- a/packages/miniflare/src/workers/core/entry.worker.ts +++ b/packages/miniflare/src/workers/core/entry.worker.ts @@ -518,24 +518,7 @@ export default >{ return await imagesDelivery.fetch(request); } if (env[CoreBindings.TRIGGER_HANDLERS]) { - if ( - url.pathname === CorePaths.SCHEDULED || - /* legacy URL path */ url.pathname === CorePaths.LEGACY_SCHEDULED - ) { - if (url.pathname === CorePaths.LEGACY_SCHEDULED) { - ctx.waitUntil( - env[CoreBindings.SERVICE_LOOPBACK].fetch( - "http://localhost/core/log", - { - method: "POST", - headers: { - [SharedHeaders.LOG_LEVEL]: LogLevel.WARN.toString(), - }, - body: `Triggering scheduled handlers via a request to \`${CorePaths.LEGACY_SCHEDULED}\` is deprecated, and will be removed in a future version of Miniflare. Instead, send a request to \`${CorePaths.SCHEDULED}\``, - } - ) - ); - } + if (url.pathname === CorePaths.SCHEDULED) { return await handleScheduled(url.searchParams, service); } @@ -548,13 +531,6 @@ export default >{ ctx ); } - - if (url.pathname.startsWith(CorePaths.HANDLER_PREFIX)) { - return new Response( - `"${url.pathname}" is not a valid handler. Did you mean to use "${CorePaths.SCHEDULED}" or "${CorePaths.EMAIL}"?`, - { status: 404 } - ); - } } const streamService = env[CoreBindings.SERVICE_STREAM]; @@ -575,6 +551,13 @@ export default >{ return await r2PublicService.fetch(request); } + if (url.pathname.startsWith(CorePaths.LOCAL_PREFIX)) { + return new Response( + `"${url.pathname}" is not a recognised local endpoint.`, + { status: 404 } + ); + } + let response = await service.fetch(request); if (!disablePrettyErrorPage) { response = await maybePrettifyError(request, response, env); diff --git a/packages/miniflare/src/workers/images/images.worker.ts b/packages/miniflare/src/workers/images/images.worker.ts index 1f47dfaba8..5715b2af34 100644 --- a/packages/miniflare/src/workers/images/images.worker.ts +++ b/packages/miniflare/src/workers/images/images.worker.ts @@ -251,7 +251,7 @@ export default class ImagesService extends WorkerEntrypoint { async fetch(request: Request): Promise { const url = new URL(request.url); - // Serve image bytes at /cdn-cgi/mf/imagedelivery// + // Serve image bytes at /__cf_local/imagedelivery// if (url.pathname.startsWith(`${CorePaths.IMAGE_DELIVERY}/`)) { const parts = url.pathname .slice(CorePaths.IMAGE_DELIVERY.length + 1) diff --git a/packages/miniflare/src/workers/local-explorer/generated/types.gen.ts b/packages/miniflare/src/workers/local-explorer/generated/types.gen.ts index 215f2aab52..7d76dd7913 100644 --- a/packages/miniflare/src/workers/local-explorer/generated/types.gen.ts +++ b/packages/miniflare/src/workers/local-explorer/generated/types.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts export type ClientOptions = { - baseUrl: `${string}://${string}/cdn-cgi/explorer/api` | (string & {}); + baseUrl: `${string}://${string}/cdn-cgi/local/explorer/api` | (string & {}); }; export type R2V4Response = { diff --git a/packages/miniflare/src/workers/local-explorer/openapi.local.json b/packages/miniflare/src/workers/local-explorer/openapi.local.json index 47357fcdbb..310013567b 100644 --- a/packages/miniflare/src/workers/local-explorer/openapi.local.json +++ b/packages/miniflare/src/workers/local-explorer/openapi.local.json @@ -8,7 +8,7 @@ "servers": [ { "description": "Local Explorer", - "url": "/cdn-cgi/explorer/api" + "url": "/cdn-cgi/local/explorer/api" } ], "paths": { diff --git a/packages/miniflare/src/workers/local-explorer/route-names.ts b/packages/miniflare/src/workers/local-explorer/route-names.ts index f6afe51f55..c5a21a7805 100644 --- a/packages/miniflare/src/workers/local-explorer/route-names.ts +++ b/packages/miniflare/src/workers/local-explorer/route-names.ts @@ -36,8 +36,8 @@ const ROUTE_PATTERNS: [RegExp, string][] = [ * Strips IDs and converts to dot notation. */ export function getRouteName(path: string): string { - // Remove /cdn-cgi/explorer/api prefix - const apiPath = path.replace(/^\/cdn-cgi\/explorer\/api/, ""); + // Remove /cdn-cgi/local/explorer/api prefix + const apiPath = path.replace(/^\/cdn-cgi\/local\/explorer\/api/, ""); for (const [pattern, name] of ROUTE_PATTERNS) { if (pattern.test(apiPath)) { diff --git a/packages/miniflare/src/workers/stream/binding.worker.ts b/packages/miniflare/src/workers/stream/binding.worker.ts index aea8246612..739f8292e3 100644 --- a/packages/miniflare/src/workers/stream/binding.worker.ts +++ b/packages/miniflare/src/workers/stream/binding.worker.ts @@ -32,7 +32,7 @@ function rowsToDownloadResponse( export class StreamBinding extends WorkerEntrypoint { async fetch(request: Request): Promise { const url = new URL(request.url); - const match = url.pathname.match(/^\/cdn-cgi\/mf\/stream\/([^/]+)\/watch$/); + const match = url.pathname.match(/^\/__cf_local\/stream\/([^/]+)\/watch$/); if (!match) { return new Response("Not found", { status: 404 }); } diff --git a/packages/miniflare/src/workers/stream/schemas.ts b/packages/miniflare/src/workers/stream/schemas.ts index 61f09831a7..e4fb90b76a 100644 --- a/packages/miniflare/src/workers/stream/schemas.ts +++ b/packages/miniflare/src/workers/stream/schemas.ts @@ -1,3 +1,5 @@ +import { CorePaths } from "../core/constants"; + export const SQL_SCHEMA = ` CREATE TABLE IF NOT EXISTS _mf_stream_videos ( id TEXT PRIMARY KEY, @@ -130,7 +132,7 @@ export type DownloadRow = { export function rowToStreamVideo(row: VideoRow, entryUrl: URL): StreamVideo { const placeholderUrl = `https://customer-placeholder.cloudflarestream.com/${row.id}`; - const videoUrl = `${entryUrl.origin}/cdn-cgi/mf/stream/${row.id}/watch`; + const videoUrl = `${entryUrl.origin}${CorePaths.STREAM_VIDEO}/${row.id}/watch`; return { id: row.id, creator: row.creator, diff --git a/packages/miniflare/test/index.spec.ts b/packages/miniflare/test/index.spec.ts index cfd37413fe..79923b206b 100644 --- a/packages/miniflare/test/index.spec.ts +++ b/packages/miniflare/test/index.spec.ts @@ -1711,11 +1711,11 @@ test("Miniflare: manually triggered scheduled events", async ({ expect }) => { let res = await mf.dispatchFetch("http://localhost"); expect(await res.text()).toBe("false"); - res = await mf.dispatchFetch("http://localhost/cdn-cgi/handler/scheduled"); + res = await mf.dispatchFetch("http://localhost/cdn-cgi/local/scheduled"); expect(await res.text()).toBe("ok"); res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/scheduled?format=json" + "http://localhost/cdn-cgi/local/scheduled?format=json" ); expect(await res.json()).toEqual({ outcome: "ok", noRetry: true }); @@ -1782,7 +1782,7 @@ test("Miniflare: manually triggered scheduled events with assets", async ({ expect(res.headers.get("content-type")).toBe("text/markdown; charset=utf-8"); expect(await res.text()).toBe("asset"); - res = await mf.dispatchFetch("http://localhost/cdn-cgi/handler/scheduled"); + res = await mf.dispatchFetch("http://localhost/cdn-cgi/local/scheduled"); expect(await res.text()).toBe("ok"); res = await mf.dispatchFetch("http://localhost"); @@ -1792,7 +1792,7 @@ test("Miniflare: manually triggered scheduled events with assets", async ({ expect(json.scheduledTime).toBeDefined(); res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/scheduled?format=json&cron=0+0+0+0+0&time=1234567890987" + "http://localhost/cdn-cgi/local/scheduled?format=json&cron=0+0+0+0+0&time=1234567890987" ); expect(await res.json()).toEqual({ outcome: "ok", @@ -1832,7 +1832,7 @@ test("Miniflare: manually triggered email handler - valid email", async ({ expect(await res.text()).toBe("false"); res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?from=someone@example.com&to=someone-else@example.com", + "http://localhost/cdn-cgi/local/email?from=someone@example.com&to=someone-else@example.com", { body: `From: someone To: someone else @@ -1879,7 +1879,7 @@ test("Miniflare: manually triggered email handler - setReject does not throw", a expect(await res.text()).toBe("false"); res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?from=someone@example.com&to=someone-else@example.com", + "http://localhost/cdn-cgi/local/email?from=someone@example.com&to=someone-else@example.com", { body: `From: someone To: someone else @@ -1928,7 +1928,7 @@ test("Miniflare: manually triggered email handler - forward does not throw", asy expect(await res.text()).toBe("false"); res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?from=someone@example.com&to=someone-else@example.com", + "http://localhost/cdn-cgi/local/email?from=someone@example.com&to=someone-else@example.com", { body: `From: someone To: someone else @@ -1974,7 +1974,7 @@ test("Miniflare: manually triggered email handler - invalid email, no message id expect(await res.text()).toBe("false"); res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?from=someone@example.com&to=someone-else@example.com", + "http://localhost/cdn-cgi/local/email?from=someone@example.com&to=someone-else@example.com", { body: `From: someone To: someone else @@ -2037,7 +2037,7 @@ This is a random email body. expect(await res.text()).toBe("false"); res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?from=someone@example.com&to=someone-else@example.com", + "http://localhost/cdn-cgi/local/email?from=someone@example.com&to=someone-else@example.com", { body: `From: someone To: someone else @@ -2057,9 +2057,7 @@ This is a random email body. expect(await res.text()).toBe("true"); }); -test("Miniflare: unimplemented /cdn-cgi/handler/ routes", async ({ - expect, -}) => { +test("Miniflare: unrecognised /cdn-cgi/local/ routes", async ({ expect }) => { const mf = new Miniflare({ modules: true, script: ` @@ -2073,9 +2071,9 @@ test("Miniflare: unimplemented /cdn-cgi/handler/ routes", async ({ }); useDispose(mf); - const res = await mf.dispatchFetch("http://localhost/cdn-cgi/handler/foo"); + const res = await mf.dispatchFetch("http://localhost/cdn-cgi/local/foo"); expect(await res.text()).toBe( - `"/cdn-cgi/handler/foo" is not a valid handler. Did you mean to use "/cdn-cgi/handler/scheduled" or "/cdn-cgi/handler/email"?` + `"/cdn-cgi/local/foo" is not a recognised local endpoint.` ); expect(res.status).toBe(404); }); @@ -2137,7 +2135,7 @@ test("Miniflare: blocks non-local Host headers from reaching /cdn-cgi/ routes", setHost: false, headers: { Host: "example.trycloudflare.com", - "MF-Original-URL": "http://localhost/cdn-cgi/handler/scheduled", + "MF-Original-URL": "http://localhost/cdn-cgi/local/scheduled", }, }, (res) => { diff --git a/packages/miniflare/test/plugins/email/index.spec.ts b/packages/miniflare/test/plugins/email/index.spec.ts index 17618484f6..f8eaeafc0c 100644 --- a/packages/miniflare/test/plugins/email/index.spec.ts +++ b/packages/miniflare/test/plugins/email/index.spec.ts @@ -455,7 +455,7 @@ test("reply validation: x-auto-response-suppress", async ({ expect }) => { This is a random email body.`; const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?" + + "http://localhost/cdn-cgi/local/email?" + new URLSearchParams({ from: "someone@example.com", to: "someone-else@example.com", @@ -495,7 +495,7 @@ test("reply validation: Auto-Submitted", async ({ expect }) => { This is a random email body.`; const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?" + + "http://localhost/cdn-cgi/local/email?" + new URLSearchParams({ from: "someone@example.com", to: "someone-else@example.com", @@ -535,7 +535,7 @@ test("reply validation: only In-Reply-To", async ({ expect }) => { This is a random email body.`; const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?" + + "http://localhost/cdn-cgi/local/email?" + new URLSearchParams({ from: "someone@example.com", to: "someone-else@example.com", @@ -575,7 +575,7 @@ test("reply validation: only References", async ({ expect }) => { This is a random email body.`; const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?" + + "http://localhost/cdn-cgi/local/email?" + new URLSearchParams({ from: "someone@example.com", to: "someone-else@example.com", @@ -616,7 +616,7 @@ test("reply validation: >100 References", async ({ expect }) => { This is a random email body.`; const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?" + + "http://localhost/cdn-cgi/local/email?" + new URLSearchParams({ from: "someone@example.com", to: "someone-else@example.com", @@ -659,7 +659,7 @@ test("reply: mismatched From: header", async ({ expect }) => { This is a random email body.`; const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?" + + "http://localhost/cdn-cgi/local/email?" + new URLSearchParams({ from: "someone@example.com", to: "someone-else@example.com", @@ -699,7 +699,7 @@ test("reply: unparseable", async ({ expect }) => { This is a random email body.`; const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?" + + "http://localhost/cdn-cgi/local/email?" + new URLSearchParams({ from: "someone@example.com", to: "someone-else@example.com", @@ -747,7 +747,7 @@ test("reply: no message id", async ({ expect }) => { This is a random email body.`; const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?" + + "http://localhost/cdn-cgi/local/email?" + new URLSearchParams({ from: "someone@example.com", to: "someone-else@example.com", @@ -797,7 +797,7 @@ test("reply: disallowed header", async ({ expect }) => { This is a random email body.`; const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?" + + "http://localhost/cdn-cgi/local/email?" + new URLSearchParams({ from: "someone@example.com", to: "someone-else@example.com", @@ -846,7 +846,7 @@ test("reply: missing In-Reply-To", async ({ expect }) => { This is a random email body.`; const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?" + + "http://localhost/cdn-cgi/local/email?" + new URLSearchParams({ from: "someone@example.com", to: "someone-else@example.com", @@ -898,7 +898,7 @@ test("reply: wrong In-Reply-To", async ({ expect }) => { This is a random email body.`; const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?" + + "http://localhost/cdn-cgi/local/email?" + new URLSearchParams({ from: "someone@example.com", to: "someone-else@example.com", @@ -953,7 +953,7 @@ test("reply: invalid references", async ({ expect }) => { This is a random email body.`; const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?" + + "http://localhost/cdn-cgi/local/email?" + new URLSearchParams({ from: "someone@example.com", to: "someone-else@example.com", @@ -1002,7 +1002,7 @@ test("reply: references generated correctly", async ({ expect }) => { This is a random email body.`; const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/handler/email?" + + "http://localhost/cdn-cgi/local/email?" + new URLSearchParams({ from: "someone@example.com", to: "someone-else@example.com", diff --git a/packages/miniflare/test/plugins/images/index.spec.ts b/packages/miniflare/test/plugins/images/index.spec.ts index 178d2e3f21..2a21050285 100644 --- a/packages/miniflare/test/plugins/images/index.spec.ts +++ b/packages/miniflare/test/plugins/images/index.spec.ts @@ -93,7 +93,7 @@ function upload( } describe("Images local delivery", () => { - test("variant URLs are absolute and use /cdn-cgi/mf/imagedelivery/ path", async ({ + test("variant URLs are absolute and use /__cf_local/imagedelivery/ path", async ({ expect, }) => { const mf = createMiniflare(); @@ -103,7 +103,7 @@ describe("Images local delivery", () => { const metadata = await upload(mf, TEST_IMAGE_BYTES, { id: "variant-test" }); expect(metadata.variants).toHaveLength(1); expect(metadata.variants[0]).toBe( - `${url.origin}/cdn-cgi/mf/imagedelivery/variant-test/public` + `${url.origin}/__cf_local/imagedelivery/variant-test/public` ); }); @@ -115,7 +115,7 @@ describe("Images local delivery", () => { await upload(mf, TEST_IMAGE_BYTES, { id: "delivery-test" }); const response = await mf.dispatchFetch( - `${url.origin}/cdn-cgi/mf/imagedelivery/delivery-test/public` + `${url.origin}/__cf_local/imagedelivery/delivery-test/public` ); expect(response.status).toBe(200); const data = new Uint8Array(await response.arrayBuffer()); @@ -130,7 +130,7 @@ describe("Images local delivery", () => { const url = await mf.ready; const response = await mf.dispatchFetch( - `${url.origin}/cdn-cgi/mf/imagedelivery/does-not-exist/public` + `${url.origin}/__cf_local/imagedelivery/does-not-exist/public` ); expect(response.status).toBe(404); await response.arrayBuffer(); diff --git a/packages/miniflare/test/plugins/local-explorer/index.spec.ts b/packages/miniflare/test/plugins/local-explorer/index.spec.ts index 2b492d4f8d..3c9e896931 100644 --- a/packages/miniflare/test/plugins/local-explorer/index.spec.ts +++ b/packages/miniflare/test/plugins/local-explorer/index.spec.ts @@ -259,9 +259,11 @@ describe("Local Explorer API validation", () => { }); describe("routing", () => { - test("serves OpenAPI spec at /cdn-cgi/explorer/api", async ({ expect }) => { + test("serves OpenAPI spec at /cdn-cgi/local/explorer/api", async ({ + expect, + }) => { const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/explorer/api" + "http://localhost/cdn-cgi/local/explorer/api" ); expect(res.status).toBe(200); expect(res.headers.get("Content-Type")).toContain("application/json"); @@ -273,31 +275,39 @@ describe("Local Explorer API validation", () => { }); }); - test("serves explorer UI at /cdn-cgi/explorer", async ({ expect }) => { - const res = await mf.dispatchFetch("http://localhost/cdn-cgi/explorer"); + test("serves explorer UI at /cdn-cgi/local/explorer", async ({ + expect, + }) => { + const res = await mf.dispatchFetch( + "http://localhost/cdn-cgi/local/explorer" + ); expect(res.status).toBe(200); expect(res.headers.get("Content-Type")).toContain("text/html"); await res.arrayBuffer(); // Drain }); - test("serves explorer UI at /cdn-cgi/explorer/", async ({ expect }) => { - const res = await mf.dispatchFetch("http://localhost/cdn-cgi/explorer/"); + test("serves explorer UI at /cdn-cgi/local/explorer/", async ({ + expect, + }) => { + const res = await mf.dispatchFetch( + "http://localhost/cdn-cgi/local/explorer/" + ); expect(res.status).toBe(200); expect(res.headers.get("Content-Type")).toContain("text/html"); await res.arrayBuffer(); // Drain }); - test("does not match paths that start with /cdn-cgi/explorer but are not the explorer", async ({ + test("does not match paths that start with /cdn-cgi/local/explorer but are not the explorer", async ({ expect, }) => { - // This should fall through to the user worker, not match the explorer + // /cdn-cgi/local/explorerfoo is not a recognised local endpoint const res = await mf.dispatchFetch( - "http://localhost/cdn-cgi/explorerfoo" + "http://localhost/cdn-cgi/local/explorerfoo" ); - expect(res.status).toBe(200); - expect(await res.text()).toBe("user worker"); + expect(res.status).toBe(404); + await res.arrayBuffer(); // Drain }); }); }); diff --git a/packages/miniflare/test/plugins/local-explorer/telemetry.spec.ts b/packages/miniflare/test/plugins/local-explorer/telemetry.spec.ts index 501a0c8ca3..6f2b28ad7a 100644 --- a/packages/miniflare/test/plugins/local-explorer/telemetry.spec.ts +++ b/packages/miniflare/test/plugins/local-explorer/telemetry.spec.ts @@ -15,7 +15,7 @@ describe("getRouteName", () => { test(`handles ${method.toUpperCase()} ${path}`, ({ expect }) => { // Replace {param} placeholders with dummy values const testPath = path.replace(/\{[^}]+\}/g, "test-id"); - const fullPath = `/cdn-cgi/explorer/api${testPath}`; + const fullPath = `/cdn-cgi/local/explorer/api${testPath}`; const routeName = getRouteName(fullPath); @@ -27,12 +27,14 @@ describe("getRouteName", () => { }); test("maps routes to expected names", ({ expect }) => { - expect(getRouteName(`/cdn-cgi/explorer/api/storage/kv/namespaces`)).toBe( - "kv.namespaces" - ); + expect( + getRouteName(`/cdn-cgi/local/explorer/api/storage/kv/namespaces`) + ).toBe("kv.namespaces"); }); test("returns unknown for unrecognized paths", ({ expect }) => { - expect(getRouteName("/cdn-cgi/explorer/api/unknown/path")).toBe("unknown"); + expect(getRouteName("/cdn-cgi/local/explorer/api/unknown/path")).toBe( + "unknown" + ); }); }); diff --git a/packages/miniflare/test/plugins/stream/index.spec.ts b/packages/miniflare/test/plugins/stream/index.spec.ts index 33ed4a1a9a..202c2a94db 100644 --- a/packages/miniflare/test/plugins/stream/index.spec.ts +++ b/packages/miniflare/test/plugins/stream/index.spec.ts @@ -211,7 +211,7 @@ describe("Stream videos", () => { expect(video.hlsPlaybackUrl).toContain(video.id); expect(video.dashPlaybackUrl).toContain(video.id); expect(video.preview).toMatch( - new RegExp(`^http://.*?/cdn-cgi/mf/stream/${video.id}/watch$`) + new RegExp(`^http://.*?/__cf_local/stream/${video.id}/watch$`) ); const details = (await sendCmdToWorker(mf, "video.details", { @@ -646,7 +646,7 @@ describe("Stream videos list", () => { }); describe("Stream video serving", () => { - test("serve video via /cdn-cgi/mf/stream/:id/watch", async ({ expect }) => { + test("serve video via /__cf_local/stream/:id/watch", async ({ expect }) => { const mf = createMiniflare(); useDispose(mf); const { http: videoUrl } = await useServer( @@ -659,7 +659,7 @@ describe("Stream video serving", () => { // Fetch the video via the preview URL path and consume body immediately const resp = await mf.dispatchFetch( - `http://placeholder/cdn-cgi/mf/stream/${video.id}/watch` + `http://placeholder/__cf_local/stream/${video.id}/watch` ); const bytes = new Uint8Array(await resp.arrayBuffer()); expect(resp.status).toBe(200); @@ -671,7 +671,7 @@ describe("Stream video serving", () => { useDispose(mf); const resp = await mf.dispatchFetch( - "http://placeholder/cdn-cgi/mf/stream/00000000-0000-0000-0000-000000000000/watch" + "http://placeholder/__cf_local/stream/00000000-0000-0000-0000-000000000000/watch" ); await resp.arrayBuffer(); // consume body to avoid dispatchFetch error expect(resp.status).toBe(404); @@ -1626,7 +1626,7 @@ describe("Stream publicUrl", () => { })) as Video; expect(video.preview).toBe( - `http://my-proxy.example.com:8080/cdn-cgi/mf/stream/${video.id}/watch` + `http://my-proxy.example.com:8080/__cf_local/stream/${video.id}/watch` ); }); @@ -1647,7 +1647,7 @@ describe("Stream publicUrl", () => { // (http://127.0.0.1:) rather than any external proxy URL expect(video.preview).toMatch( new RegExp( - `^http://127\\.0\\.0\\.1:\\d+/cdn-cgi/mf/stream/${video.id}/watch$` + `^http://127\\.0\\.0\\.1:\\d+/__cf_local/stream/${video.id}/watch$` ) ); }); From a6902734bdbbec9872a6ceda8db63dae6e8acefd Mon Sep 17 00:00:00 2001 From: emily-shen Date: Thu, 9 Jul 2026 13:32:06 +0100 Subject: [PATCH 3/6] rewrite paths in wrangler/vite --- .../rewrite-legacy-local-testing-paths.md | 18 +++++ fixtures/worker-app/src/index.js | 5 +- .../local-explorer-ui/src/__e2e__/utils.ts | 4 +- .../bindings/__tests__/worker.spec.ts | 4 +- .../__tests__/cron-triggers.spec.ts | 19 +++--- .../__tests__/email-triggers.spec.ts | 68 ++++++++++--------- .../stream-binding/__tests__/worker.spec.ts | 2 +- .../src/__tests__/shortcuts.spec.ts | 4 +- .../src/plugins/trigger-handlers.ts | 43 ++++++++++-- .../src/environment-variables/factory.ts | 2 +- .../environment-variables/misc-variables.ts | 2 +- .../wrangler/e2e/createTestHarness.test.ts | 4 +- packages/wrangler/e2e/dev.test.ts | 53 ++++++++------- packages/wrangler/e2e/multiworker-dev.test.ts | 2 +- .../wrangler/src/__tests__/workflows.test.ts | 4 +- packages/wrangler/src/api/test-harness.ts | 2 +- packages/wrangler/src/dev/start-dev.ts | 2 +- packages/wrangler/src/workflows/local.ts | 2 +- .../templates/new-worker-scheduled.js | 2 +- .../templates/new-worker-scheduled.ts | 2 +- .../templates/startDevWorker/ProxyWorker.ts | 27 ++++++++ 21 files changed, 183 insertions(+), 88 deletions(-) create mode 100644 .changeset/rewrite-legacy-local-testing-paths.md diff --git a/.changeset/rewrite-legacy-local-testing-paths.md b/.changeset/rewrite-legacy-local-testing-paths.md new file mode 100644 index 0000000000..bc0a1f6e94 --- /dev/null +++ b/.changeset/rewrite-legacy-local-testing-paths.md @@ -0,0 +1,18 @@ +--- +"wrangler": patch +"@cloudflare/vite-plugin": patch +--- + +Rewrite local testing paths (`/cdn-cgi/*`) + +Miniflare v5 moved its internal local testing endpoints to `/cdn-cgi/local/*` (and `/__cf_local/*` for endpoints that must remain reachable over tunnels) to prevent any potential collision with production routes. `wrangler dev` and the Vite plugin now transparently rewrite the old paths to the new ones, meaning you can continue to use the old paths without issue. + +These are the new paths: + +- `/cdn-cgi/handler/scheduled` → `/cdn-cgi/local/scheduled` +- `/cdn-cgi/handler/email` → `/cdn-cgi/local/email` +- `/cdn-cgi/explorer/*` → `/cdn-cgi/local/explorer/*` +- `/cdn-cgi/mf/scheduled` → `/cdn-cgi/local/scheduled` (Note `/cdn-cgi/mf/scheduled` is already deprecated) +- `/cdn-cgi/mf/stream/*` → `/__cf_local/stream/*` +- `/cdn-cgi/mf/imagedelivery/*` → `/__cf_local/imagedelivery/*` +- `/cdn-cgi/platform-proxy` → `/cdn-cgi/local/platform-proxy` diff --git a/fixtures/worker-app/src/index.js b/fixtures/worker-app/src/index.js index 2ecac739f2..58e8cba16f 100644 --- a/fixtures/worker-app/src/index.js +++ b/fixtures/worker-app/src/index.js @@ -94,9 +94,8 @@ export default { /** * Handle a scheduled event. * - * If developing using `--local` mode, you can trigger this scheduled event via a CURL. - * E.g. `curl "http://localhost:8787/cdn-cgi/mf/scheduled"`. - * See the Miniflare docs: https://miniflare.dev/core/scheduled. + * When developing locally, you can trigger this scheduled event via a CURL. + * E.g. `curl "http://localhost:8787/cdn-cgi/local/scheduled"`. */ scheduled(event, env, ctx) { ctx.waitUntil(Promise.resolve(event.scheduledTime)); diff --git a/packages/local-explorer-ui/src/__e2e__/utils.ts b/packages/local-explorer-ui/src/__e2e__/utils.ts index 79ecf82399..747b3b410d 100644 --- a/packages/local-explorer-ui/src/__e2e__/utils.ts +++ b/packages/local-explorer-ui/src/__e2e__/utils.ts @@ -207,7 +207,9 @@ export async function navigateToDOObjectByName( // Extract the object ID from the current URL after navigation. const objectPath = new URL(page.url()).pathname; - const match = objectPath.match(/\/cdn-cgi\/explorer\/do\/[^/]+\/([^/?#]+)/); + const match = objectPath.match( + /\/cdn-cgi\/local\/explorer\/do\/[^/]+\/([^/?#]+)/ + ); if (!match || !match[1]) { throw new Error(`Could not extract object ID from URL path: ${objectPath}`); } diff --git a/packages/vite-plugin-cloudflare/playground/bindings/__tests__/worker.spec.ts b/packages/vite-plugin-cloudflare/playground/bindings/__tests__/worker.spec.ts index 3615cdba07..266afd0648 100644 --- a/packages/vite-plugin-cloudflare/playground/bindings/__tests__/worker.spec.ts +++ b/packages/vite-plugin-cloudflare/playground/bindings/__tests__/worker.spec.ts @@ -2,7 +2,9 @@ import { test } from "vitest"; import { getResponse, getTextResponse } from "../../__test-utils__"; test("serves Local Explorer UI", async ({ expect }) => { - const response = await getResponse("/cdn-cgi/explorer"); + let response = await getResponse("/cdn-cgi/local/explorer"); + expect(response.status()).toBe(200); + response = await getResponse("/cdn-cgi/explorer"); expect(response.status()).toBe(200); }); diff --git a/packages/vite-plugin-cloudflare/playground/cron-triggers/__tests__/cron-triggers.spec.ts b/packages/vite-plugin-cloudflare/playground/cron-triggers/__tests__/cron-triggers.spec.ts index 5b940cd0e1..83aee80d06 100644 --- a/packages/vite-plugin-cloudflare/playground/cron-triggers/__tests__/cron-triggers.spec.ts +++ b/packages/vite-plugin-cloudflare/playground/cron-triggers/__tests__/cron-triggers.spec.ts @@ -1,10 +1,13 @@ -import { test } from "vitest"; +import { describe, test } from "vitest"; import { getTextResponse, serverLogs } from "../../__test-utils__"; -test("Supports testing Cron Triggers at '/cdn-cgi/handler/scheduled' route", async ({ - expect, -}) => { - const cronResponse = await getTextResponse("/cdn-cgi/handler/scheduled"); - expect(cronResponse).toBe("ok"); - expect(serverLogs.info.join()).toContain("Cron processed"); -}); +describe.each(["/cdn-cgi/local/scheduled", "/cdn-cgi/handler/scheduled"])( + "%s", + (path) => { + test("Supports testing Cron Triggers", async ({ expect }) => { + const cronResponse = await getTextResponse(path); + expect(cronResponse).toBe("ok"); + expect(serverLogs.info.join()).toContain("Cron processed"); + }); + } +); diff --git a/packages/vite-plugin-cloudflare/playground/email-worker/__tests__/email-triggers.spec.ts b/packages/vite-plugin-cloudflare/playground/email-worker/__tests__/email-triggers.spec.ts index 03d5424dbd..7cef711a3e 100644 --- a/packages/vite-plugin-cloudflare/playground/email-worker/__tests__/email-triggers.spec.ts +++ b/packages/vite-plugin-cloudflare/playground/email-worker/__tests__/email-triggers.spec.ts @@ -1,5 +1,5 @@ import dedent from "ts-dedent"; -import { test } from "vitest"; +import { describe, test } from "vitest"; import { getTextResponse, serverLogs, viteTestUrl } from "../../__test-utils__"; test("Supports sending email via the email binding", async ({ expect }) => { @@ -7,38 +7,40 @@ test("Supports sending email via the email binding", async ({ expect }) => { expect(sendEmailResponse).toBe("Email message sent successfully!"); }); -test("Supports testing Email Workers at '/cdn-cgi/handler/scheduled' route", async ({ - expect, -}) => { - const params = new URLSearchParams(); - params.append("from", "sender@example.com"); - params.append("to", "recipient@example.com"); +// The canonical path is `/cdn-cgi/local/email`; `/cdn-cgi/handler/email` is the +// legacy path kept working via a rewrite in the trigger-handlers plugin. +describe.each(["/cdn-cgi/local/email", "/cdn-cgi/handler/email"])( + "%s", + (path) => { + test("Supports testing Email Workers", async ({ expect }) => { + const params = new URLSearchParams(); + params.append("from", "sender@example.com"); + params.append("to", "recipient@example.com"); - const fetchResponse = await fetch( - `${viteTestUrl}/cdn-cgi/handler/email?${params}`, - { - method: "POST", - body: dedent` - From: "John" - Reply-To: sender@example.com - To: recipient@example.com - Subject: Testing Email Workers Local Dev - Content-Type: text/html; charset="windows-1252" - X-Mailer: Curl - Date: Tue, 27 Aug 2024 08:49:44 -0700 - Message-ID: <6114391943504294873000@ZSH-GHOSTTY> + const fetchResponse = await fetch(`${viteTestUrl}${path}?${params}`, { + method: "POST", + body: dedent` + From: "John" + Reply-To: sender@example.com + To: recipient@example.com + Subject: Testing Email Workers Local Dev + Content-Type: text/html; charset="windows-1252" + X-Mailer: Curl + Date: Tue, 27 Aug 2024 08:49:44 -0700 + Message-ID: <6114391943504294873000@ZSH-GHOSTTY> - Hi there - `, - } - ); + Hi there + `, + }); - const emailStdout = serverLogs.info.join(); - expect(await fetchResponse.text()).toBe( - "Worker successfully processed email" - ); - expect(emailStdout).toContain( - `Received email from sender@example.com on ${new Date(" 27 Aug 2024 08:49:44 -0700").toISOString()} with following message:` - ); - expect(emailStdout).toContain("Hi there"); -}); + const emailStdout = serverLogs.info.join(); + expect(await fetchResponse.text()).toBe( + "Worker successfully processed email" + ); + expect(emailStdout).toContain( + `Received email from sender@example.com on ${new Date(" 27 Aug 2024 08:49:44 -0700").toISOString()} with following message:` + ); + expect(emailStdout).toContain("Hi there"); + }); + } +); diff --git a/packages/vite-plugin-cloudflare/playground/stream-binding/__tests__/worker.spec.ts b/packages/vite-plugin-cloudflare/playground/stream-binding/__tests__/worker.spec.ts index 2c851f2785..e8e86e3c9a 100644 --- a/packages/vite-plugin-cloudflare/playground/stream-binding/__tests__/worker.spec.ts +++ b/packages/vite-plugin-cloudflare/playground/stream-binding/__tests__/worker.spec.ts @@ -8,7 +8,7 @@ test("stream upload returns a valid preview URL", async ({ expect }) => { id: string; }; expect(result.id).toBeTruthy(); - expect(result.preview).toContain("/cdn-cgi/mf/stream/"); + expect(result.preview).toContain("/__cf_local/stream/"); expect(result.preview).toContain("/watch"); }); diff --git a/packages/vite-plugin-cloudflare/src/__tests__/shortcuts.spec.ts b/packages/vite-plugin-cloudflare/src/__tests__/shortcuts.spec.ts index 55480575ee..63a50f269e 100644 --- a/packages/vite-plugin-cloudflare/src/__tests__/shortcuts.spec.ts +++ b/packages/vite-plugin-cloudflare/src/__tests__/shortcuts.spec.ts @@ -264,7 +264,9 @@ describe.skipIf(!satisfiesMinimumViteVersion("7.2.7"))("shortcuts", () => { await explorerShortcut?.action?.(mockServer); expect(mockOpen).toHaveBeenCalledWith( - expect.stringMatching(/^http:\/\/localhost:\d+\/cdn-cgi\/explorer$/) + expect.stringMatching( + /^http:\/\/localhost:\d+\/cdn-cgi\/local\/explorer$/ + ) ); }); diff --git a/packages/vite-plugin-cloudflare/src/plugins/trigger-handlers.ts b/packages/vite-plugin-cloudflare/src/plugins/trigger-handlers.ts index 961f1c28cd..1c779089b8 100644 --- a/packages/vite-plugin-cloudflare/src/plugins/trigger-handlers.ts +++ b/packages/vite-plugin-cloudflare/src/plugins/trigger-handlers.ts @@ -1,8 +1,22 @@ -import { CoreHeaders } from "miniflare"; +import { CoreHeaders, Request as MiniflareRequest } from "miniflare"; import { createPlugin, createRequestHandler } from "../utils"; +// Miniflare v5 moved its internal endpoints under `/cdn-cgi/local/` (and +// `/__cf_local/` for endpoints that must remain reachable over tunnels). These +// map the pre-v5 paths onto their current equivalents. This must stay in sync +// with `rewriteLegacyMiniflarePath()` in Wrangler's ProxyWorker. +const LEGACY_PATH_REWRITES: readonly [string, string][] = [ + ["/cdn-cgi/handler", "/cdn-cgi/local"], + ["/cdn-cgi/mf/scheduled", "/cdn-cgi/local/scheduled"], + ["/cdn-cgi/mf/stream", "/__cf_local/stream"], + ["/cdn-cgi/mf/imagedelivery", "/__cf_local/imagedelivery"], + ["/cdn-cgi/explorer", "/cdn-cgi/local/explorer"], +]; + /** - * Plugin to forward `/cdn-cgi/handler/*` routes to trigger handlers in development + * Plugin to forward trigger handler routes (scheduled, email) and other + * internal Miniflare endpoints to Miniflare in development, including + * backwards-compatible rewrites for pre-v5 paths. */ export const triggerHandlersPlugin = createPlugin("trigger-handlers", (ctx) => { return { @@ -15,14 +29,33 @@ export const triggerHandlersPlugin = createPlugin("trigger-handlers", (ctx) => { } const entryWorkerName = entryWorkerConfig.name; - const requestHandler = createRequestHandler((request) => { + + function dispatch(request: MiniflareRequest) { request.headers.set(CoreHeaders.ROUTE_OVERRIDE, entryWorkerName); return ctx.miniflare.dispatchFetch(request, { redirect: "manual", }); - }); + } - viteDevServer.middlewares.use("/cdn-cgi/handler/", requestHandler); + // Canonical paths: forward directly to Miniflare. + viteDevServer.middlewares.use( + "/cdn-cgi/local/", + createRequestHandler((request) => dispatch(request)) + ); + + // Backwards compatibility: rewrite legacy paths onto their canonical + // equivalents before dispatching. + for (const [oldPrefix, newPrefix] of LEGACY_PATH_REWRITES) { + viteDevServer.middlewares.use( + oldPrefix, + createRequestHandler((request) => { + const url = new URL(request.url); + url.pathname = + newPrefix + url.pathname.slice(oldPrefix.length); + return dispatch(new MiniflareRequest(url, request)); + }) + ); + } }, }; }); diff --git a/packages/workers-utils/src/environment-variables/factory.ts b/packages/workers-utils/src/environment-variables/factory.ts index 76aa0a244d..a67b996163 100644 --- a/packages/workers-utils/src/environment-variables/factory.ts +++ b/packages/workers-utils/src/environment-variables/factory.ts @@ -112,7 +112,7 @@ type VariableNames = // ## Experimental Feature Flags - /** Enable the local explorer UI at /cdn-cgi/explorer (experimental, default: false). */ + /** Enable the local explorer UI at /cdn-cgi/local/explorer (experimental, default: false). */ | "X_LOCAL_EXPLORER" /** Open the browser in headful (visible) mode when using the Browser Run API in local dev (default: false). */ | "X_BROWSER_HEADFUL" diff --git a/packages/workers-utils/src/environment-variables/misc-variables.ts b/packages/workers-utils/src/environment-variables/misc-variables.ts index 5b0d551a49..4b48913f80 100644 --- a/packages/workers-utils/src/environment-variables/misc-variables.ts +++ b/packages/workers-utils/src/environment-variables/misc-variables.ts @@ -345,7 +345,7 @@ export const getOpenNextDeployFromEnv = getEnvironmentVariableFactory({ }); /** - * `X_LOCAL_EXPLORER` enables the local explorer UI at /cdn-cgi/explorer. + * `X_LOCAL_EXPLORER` enables the local explorer UI at /cdn-cgi/local/explorer. */ export const getLocalExplorerEnabledFromEnv = getBooleanEnvironmentVariableFactory({ diff --git a/packages/wrangler/e2e/createTestHarness.test.ts b/packages/wrangler/e2e/createTestHarness.test.ts index 0bae285270..22173c9427 100644 --- a/packages/wrangler/e2e/createTestHarness.test.ts +++ b/packages/wrangler/e2e/createTestHarness.test.ts @@ -1592,8 +1592,8 @@ describe("createTestHarness", () => { [server] startup - completed [server] fetch - GET / - started [server] fetch - GET / - 200 - [server] [scheduled-worker] scheduled - GET /cdn-cgi/handler/scheduled?format=json&cron=*+*+*+*+*&time=1700000100000 - started - [server] [scheduled-worker] scheduled - GET /cdn-cgi/handler/scheduled?format=json&cron=*+*+*+*+*&time=1700000100000 - 200 + [server] [scheduled-worker] scheduled - GET /cdn-cgi/local/scheduled?format=json&cron=*+*+*+*+*&time=1700000100000 - started + [server] [scheduled-worker] scheduled - GET /cdn-cgi/local/scheduled?format=json&cron=*+*+*+*+*&time=1700000100000 - 200 [server] fetch - GET / - started [server] fetch - GET / - 200" `); diff --git a/packages/wrangler/e2e/dev.test.ts b/packages/wrangler/e2e/dev.test.ts index 2e9108f3bb..8884da642e 100644 --- a/packages/wrangler/e2e/dev.test.ts +++ b/packages/wrangler/e2e/dev.test.ts @@ -300,7 +300,7 @@ describe.each([ "Scheduled Workers are not automatically triggered" ); expect(worker.currentOutput).toContain( - `curl "http://${hostname}:${port}/cdn-cgi/handler/scheduled"` + `curl "http://${hostname}:${port}/cdn-cgi/local/scheduled"` ); expect(worker.currentOutput).not.toContain("undefined"); }); @@ -2394,7 +2394,7 @@ This is a random email body. const { url } = await worker.waitForReady(); const response = await fetch( - `${url}/cdn-cgi/handler/email?from=someone@example.com&to=someone-else@example.com`, + `${url}/cdn-cgi/local/email?from=someone@example.com&to=someone-else@example.com`, { body: dedent` From: someone @@ -2441,15 +2441,20 @@ This is a random email body. `); }); - it("should print reject with reason", async ({ expect }) => { - const helper = new WranglerE2ETestHelper(); - await helper.seed({ - "wrangler.toml": dedent` + // The canonical path is `/cdn-cgi/local/email`; `/cdn-cgi/handler/email` is + // the legacy path kept working via a rewrite in the dev proxy. + describe.each(["/cdn-cgi/local/email", "/cdn-cgi/handler/email"])( + "%s", + (path) => { + it("should print reject with reason", async ({ expect }) => { + const helper = new WranglerE2ETestHelper(); + await helper.seed({ + "wrangler.toml": dedent` name = "${workerName}" main = "src/index.ts" compatibility_date = "2025-03-17" `, - "src/index.ts": dedent` + "src/index.ts": dedent` import { EmailMessage } from "cloudflare:email"; export default { @@ -2457,16 +2462,16 @@ This is a random email body. await emailMessage.setReject('I dont like this email') } }`, - }); + }); - const worker = helper.runLongLived("wrangler dev"); + const worker = helper.runLongLived("wrangler dev"); - const { url } = await worker.waitForReady(); + const { url } = await worker.waitForReady(); - const response = await fetch( - `${url}/cdn-cgi/handler/email?from=someone@example.com&to=someone-else@example.com`, - { - body: `From: someone + const response = await fetch( + `${url}${path}?from=someone@example.com&to=someone-else@example.com`, + { + body: `From: someone To: someone else MIME-Version: 1.0 Message-ID: @@ -2474,16 +2479,18 @@ Content-Type: text/plain This is a random email body. `, - method: "POST", - } - ); + method: "POST", + } + ); - expect(await response.text()).toMatchInlineSnapshot( - `"Worker rejected email with the following reason: I dont like this email"` - ); + expect(await response.text()).toMatchInlineSnapshot( + `"Worker rejected email with the following reason: I dont like this email"` + ); - expect(response.status).toBe(400); - }); + expect(response.status).toBe(400); + }); + } + ); it("should print forward email", async ({ expect }) => { const helper = new WranglerE2ETestHelper(); @@ -2508,7 +2515,7 @@ This is a random email body. const { url } = await worker.waitForReady(); const response = await fetch( - `${url}/cdn-cgi/handler/email?from=someone@example.com&to=someone-else@example.com`, + `${url}/cdn-cgi/local/email?from=someone@example.com&to=someone-else@example.com`, { body: `From: someone To: someone else diff --git a/packages/wrangler/e2e/multiworker-dev.test.ts b/packages/wrangler/e2e/multiworker-dev.test.ts index 5fb3d1b9f9..1d7a67f45e 100644 --- a/packages/wrangler/e2e/multiworker-dev.test.ts +++ b/packages/wrangler/e2e/multiworker-dev.test.ts @@ -643,7 +643,7 @@ describe("multiworker", () => { "Scheduled Workers are not automatically triggered" ); expect(worker.currentOutput).toContain( - `curl "http://${hostname}:${port}/cdn-cgi/handler/scheduled"` + `curl "http://${hostname}:${port}/cdn-cgi/local/scheduled"` ); expect(worker.currentOutput).not.toContain("undefined"); }); diff --git a/packages/wrangler/src/__tests__/workflows.test.ts b/packages/wrangler/src/__tests__/workflows.test.ts index 148dbc75c5..fa5b27b085 100644 --- a/packages/wrangler/src/__tests__/workflows.test.ts +++ b/packages/wrangler/src/__tests__/workflows.test.ts @@ -1364,12 +1364,12 @@ describe("wrangler workflows", () => { }); // ========================================================================= - // Local commands (--local) — hitting /cdn-cgi/explorer/api/workflows/... + // Local commands (--local) — hitting /cdn-cgi/local/explorer/api/workflows/... // ========================================================================= describe("local", () => { const LOCAL_PORT = 8787; - const LOCAL_BASE = `http://localhost:${LOCAL_PORT}/cdn-cgi/explorer/api`; + const LOCAL_BASE = `http://localhost:${LOCAL_PORT}/cdn-cgi/local/explorer/api`; describe("workflows list --local", () => { it("should list workflows from local dev session", async ({ expect }) => { diff --git a/packages/wrangler/src/api/test-harness.ts b/packages/wrangler/src/api/test-harness.ts index 785a6f5ca2..639d15e38e 100644 --- a/packages/wrangler/src/api/test-harness.ts +++ b/packages/wrangler/src/api/test-harness.ts @@ -861,7 +861,7 @@ export function createTestHarness(options?: TestHarnessOptions): TestHarness { const response = await dispatchFetch( miniflare, - `/cdn-cgi/handler/scheduled?${searchParams.toString()}`, + `/cdn-cgi/local/scheduled?${searchParams.toString()}`, undefined, workerName, "scheduled" diff --git a/packages/wrangler/src/dev/start-dev.ts b/packages/wrangler/src/dev/start-dev.ts index 09d5cbbb0e..0cedb174f2 100644 --- a/packages/wrangler/src/dev/start-dev.ts +++ b/packages/wrangler/src/dev/start-dev.ts @@ -356,7 +356,7 @@ function maybePrintScheduledWorkerWarning( logger.once.warn( `Scheduled Workers are not automatically triggered during local development.\n` + `To manually trigger a scheduled event, run:\n` + - ` curl "http://${host}:${port}/cdn-cgi/handler/scheduled"\n` + + ` curl "http://${host}:${port}/cdn-cgi/local/scheduled"\n` + `For more details, see https://developers.cloudflare.com/workers/configuration/cron-triggers/#test-cron-triggers-locally` ); } diff --git a/packages/wrangler/src/workflows/local.ts b/packages/wrangler/src/workflows/local.ts index 505872aedd..bb73fdce88 100644 --- a/packages/wrangler/src/workflows/local.ts +++ b/packages/wrangler/src/workflows/local.ts @@ -6,7 +6,7 @@ import type { WorkflowInstanceRestartFrom, } from "./types"; -const LOCAL_EXPLORER_BASE_PATH = "/cdn-cgi/explorer/api"; +const LOCAL_EXPLORER_BASE_PATH = "/cdn-cgi/local/explorer/api"; const DEFAULT_LOCAL_PORT = 8787; /** diff --git a/packages/wrangler/templates/new-worker-scheduled.js b/packages/wrangler/templates/new-worker-scheduled.js index a56d30b72a..c95c7b1c10 100644 --- a/packages/wrangler/templates/new-worker-scheduled.js +++ b/packages/wrangler/templates/new-worker-scheduled.js @@ -2,7 +2,7 @@ * Welcome to Cloudflare Workers! This is your first scheduled worker. * * - Run `wrangler dev` in your terminal to start a development server - * - Run `curl "http://localhost:8787/cdn-cgi/handler/scheduled"` to trigger the scheduled event + * - Run `curl "http://localhost:8787/cdn-cgi/local/scheduled"` to trigger the scheduled event * - Go back to the console to see what your worker has logged * - Update the Cron trigger in wrangler.toml (see https://developers.cloudflare.com/workers/configuration/cron-triggers/) * - Run `wrangler publish --name my-worker` to publish your worker diff --git a/packages/wrangler/templates/new-worker-scheduled.ts b/packages/wrangler/templates/new-worker-scheduled.ts index f7f5992dfc..67fef0d66c 100644 --- a/packages/wrangler/templates/new-worker-scheduled.ts +++ b/packages/wrangler/templates/new-worker-scheduled.ts @@ -2,7 +2,7 @@ * Welcome to Cloudflare Workers! This is your first scheduled worker. * * - Run `wrangler dev` in your terminal to start a development server - * - Run `curl "http://localhost:8787/cdn-cgi/handler/scheduled"` to trigger the scheduled event + * - Run `curl "http://localhost:8787/cdn-cgi/local/scheduled"` to trigger the scheduled event * - Go back to the console to see what your worker has logged * - Update the Cron trigger in wrangler.toml (see https://developers.cloudflare.com/workers/configuration/cron-triggers/) * - Run `wrangler deploy --name my-worker` to deploy your worker diff --git a/packages/wrangler/templates/startDevWorker/ProxyWorker.ts b/packages/wrangler/templates/startDevWorker/ProxyWorker.ts index 4d4d9ab155..7ab067659f 100644 --- a/packages/wrangler/templates/startDevWorker/ProxyWorker.ts +++ b/packages/wrangler/templates/startDevWorker/ProxyWorker.ts @@ -130,6 +130,13 @@ export class ProxyWorker implements DurableObject { request.url ); + // rewrite requests to old miniflare paths + // because wrangler cannot have a breaking change + userWorkerUrl.pathname = rewriteLegacyMiniflarePath( + userWorkerUrl.pathname + ); + innerUrl.pathname = rewriteLegacyMiniflarePath(innerUrl.pathname); + // Preserve client `Accept-Encoding`, rather than using Worker's default // of `Accept-Encoding: br, gzip` const encoding = request.cf?.clientAcceptEncoding; @@ -235,6 +242,26 @@ export class ProxyWorker implements DurableObject { function isRequestFromProxyController(req: Request, env: Env): boolean { return req.headers.get("Authorization") === env.PROXY_CONTROLLER_AUTH_SECRET; } + +// Miniflare v5 moved its internal endpoints under `/cdn-cgi/local/` (and +// `/__cf_local/` for endpoints that must remain reachable over tunnels). These +// map the pre-v5 paths onto their current equivalents. +const LEGACY_PATH_REWRITES: readonly [string, string][] = [ + ["/cdn-cgi/handler", "/cdn-cgi/local"], + ["/cdn-cgi/mf/scheduled", "/cdn-cgi/local/scheduled"], + ["/cdn-cgi/mf/stream", "/__cf_local/stream"], + ["/cdn-cgi/mf/imagedelivery", "/__cf_local/imagedelivery"], + ["/cdn-cgi/explorer", "/cdn-cgi/local/explorer"], +]; + +function rewriteLegacyMiniflarePath(pathname: string): string { + for (const [oldPrefix, newPrefix] of LEGACY_PATH_REWRITES) { + if (pathname === oldPrefix || pathname.startsWith(`${oldPrefix}/`)) { + return newPrefix + pathname.slice(oldPrefix.length); + } + } + return pathname; +} function isHtmlResponse(res: Response): boolean { return res.headers.get("content-type")?.startsWith("text/html") ?? false; } From 534bac936a069291062b09b46f9b7fd48c2f54d7 Mon Sep 17 00:00:00 2001 From: emily-shen Date: Thu, 9 Jul 2026 15:43:16 +0100 Subject: [PATCH 4/6] remove missing path 404 error message --- fixtures/worker-with-resources/tests/index.test.ts | 2 +- packages/miniflare/src/workers/core/constants.ts | 2 -- packages/miniflare/src/workers/core/entry.worker.ts | 7 ------- packages/miniflare/test/index.spec.ts | 10 +++++----- .../test/plugins/local-explorer/index.spec.ts | 6 +++--- 5 files changed, 9 insertions(+), 18 deletions(-) diff --git a/fixtures/worker-with-resources/tests/index.test.ts b/fixtures/worker-with-resources/tests/index.test.ts index 4930b1fd75..9f5bb28982 100644 --- a/fixtures/worker-with-resources/tests/index.test.ts +++ b/fixtures/worker-with-resources/tests/index.test.ts @@ -80,7 +80,7 @@ describe("local explorer", () => { const html = await indexResponse.text(); // Extract JS asset path from the HTML - // The HTML looks like: