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}); @@ -1014,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 */ @@ -1030,7 +1007,6 @@ export function getGlobalServices({ sharedOptions, allWorkerRoutes, fallbackWorkerName, - loopbackPort, tmpPath, log, proxyBindings, @@ -1141,14 +1117,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/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 4efb9455ea..1b4bd5f30a 100644 --- a/packages/miniflare/src/workers/core/constants.ts +++ b/packages/miniflare/src/workers/core/constants.ts @@ -1,26 +1,23 @@ /** - * 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/", - /** Live reload WebSocket endpoint */ - LIVE_RELOAD: "/cdn-cgi/mf/reload", + EMAIL: "/cdn-cgi/local/email", /** 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; @@ -68,7 +65,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/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 013de035eb..dae2fd1f64 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 { @@ -558,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); } @@ -588,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]; @@ -619,7 +555,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/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 a3eaaad9ea..dcd3d5523c 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(); @@ -1724,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 }); @@ -1795,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"); @@ -1805,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", @@ -1845,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 @@ -1892,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 @@ -1941,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 @@ -1987,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 @@ -2050,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 @@ -2070,7 +2057,7 @@ This is a random email body. expect(await res.text()).toBe("true"); }); -test("Miniflare: unimplemented /cdn-cgi/handler/ routes", async ({ +test("Miniflare: unrecognised /cdn-cgi/local/ routes fall through to user worker", async ({ expect, }) => { const mf = new Miniflare({ @@ -2086,11 +2073,9 @@ test("Miniflare: unimplemented /cdn-cgi/handler/ routes", async ({ }); useDispose(mf); - const res = await mf.dispatchFetch("http://localhost/cdn-cgi/handler/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"?` - ); - expect(res.status).toBe(404); + const res = await mf.dispatchFetch("http://localhost/cdn-cgi/local/foo"); + expect(await res.text()).toBe("Hello world"); + expect(res.status).toBe(200); }); test("Miniflare: other /cdn-cgi/ routes", async ({ expect }) => { @@ -2150,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..5a3941d6af 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,28 +275,36 @@ 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 falls through to the user worker 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"); 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$` ) ); }); 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/preview.ts b/packages/vite-plugin-cloudflare/src/plugins/preview.ts index 1617b295de..4ef4e1d770 100644 --- a/packages/vite-plugin-cloudflare/src/plugins/preview.ts +++ b/packages/vite-plugin-cloudflare/src/plugins/preview.ts @@ -5,13 +5,14 @@ import { } from "@cloudflare/containers-shared"; import { cleanupContainers } from "@cloudflare/containers-shared/src/utils"; import { UserError } from "@cloudflare/workers-utils"; -import { buildPublicUrl } from "miniflare"; +import { buildPublicUrl, Request as MiniflareRequest } from "miniflare"; import colors from "picocolors"; import { getDockerPath } from "../containers"; import { assertIsPreview } from "../context"; import { getPreviewMiniflareOptions } from "../miniflare-options"; import { createPlugin, createRequestHandler } from "../utils"; import { handleWebSocket } from "../websockets"; +import { rewriteLegacyMiniflarePath } from "./trigger-handlers"; let exitCallback = () => {}; @@ -121,6 +122,12 @@ export const previewPlugin = createPlugin("preview", (ctx) => { // In preview mode we put our middleware at the front of the chain so that all assets are handled in Miniflare vitePreviewServer.middlewares.use( createRequestHandler((request) => { + const url = new URL(request.url); + const rewritten = rewriteLegacyMiniflarePath(url.pathname); + if (rewritten !== url.pathname) { + url.pathname = rewritten; + request = new MiniflareRequest(url, request); + } return ctx.miniflare.dispatchFetch(request, { redirect: "manual" }); }) ); diff --git a/packages/vite-plugin-cloudflare/src/plugins/trigger-handlers.ts b/packages/vite-plugin-cloudflare/src/plugins/trigger-handlers.ts index 961f1c28cd..ae1bbb44bd 100644 --- a/packages/vite-plugin-cloudflare/src/plugins/trigger-handlers.ts +++ b/packages/vite-plugin-cloudflare/src/plugins/trigger-handlers.ts @@ -1,8 +1,31 @@ -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"], +]; + +export 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; +} + /** - * 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 +38,32 @@ 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", }); - }); + } + + // Canonical paths: forward directly to Miniflare. + viteDevServer.middlewares.use( + "/cdn-cgi/local/", + createRequestHandler((request) => dispatch(request)) + ); - viteDevServer.middlewares.use("/cdn-cgi/handler/", requestHandler); + // 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/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/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/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, 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; }