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;
}