Skip to content

Commit 1faff35

Browse files
authored
refactor(miniflare): centralize internal paths and move platform proxy endpoint (#12686)
1 parent cfd513f commit 1faff35

File tree

9 files changed

+113
-38
lines changed

9 files changed

+113
-38
lines changed

.changeset/brave-planes-dance.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"miniflare": patch
3+
---
4+
5+
Move internal proxy endpoint to reserved `/cdn-cgi/` path
6+
7+
The internal HTTP endpoint used by `getPlatformProxy` has been moved to a reserved path. This is an internal change with no impact on the `getPlatformProxy` API.

packages/miniflare/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ import {
133133
CacheHeaders,
134134
CoreBindings,
135135
CoreHeaders,
136+
CorePaths,
136137
LogLevel,
137138
Mutex,
138139
SharedHeaders,
@@ -1384,7 +1385,7 @@ export class Miniflare {
13841385
const { pathname } = new URL(req.url ?? "", "http://localhost");
13851386

13861387
// If this is the path for live-reload, handle the request
1387-
if (pathname === "/cdn-cgi/mf/reload") {
1388+
if (pathname === CorePaths.LIVE_RELOAD) {
13881389
this.#liveReloadServer.handleUpgrade(req, socket, head, (ws) => {
13891390
this.#liveReloadServer.emit("connection", ws, req);
13901391
});

packages/miniflare/src/plugins/core/constants.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { CorePaths } from "../../workers";
12
import type {
23
DoRawQueryResult,
34
DoSqlWithParams,
@@ -12,9 +13,9 @@ export const SERVICE_LOCAL_EXPLORER = `${CORE_PLUGIN_NAME}:local-explorer`;
1213
// Disk service for local explorer UI assets
1314
export const LOCAL_EXPLORER_DISK = `${CORE_PLUGIN_NAME}:local-explorer-disk`;
1415
// URL path prefix where the local explorer UI is served
15-
export const LOCAL_EXPLORER_BASE_PATH = "/cdn-cgi/explorer";
16+
export const LOCAL_EXPLORER_BASE_PATH = CorePaths.EXPLORER;
1617
// URL path prefix for the local explorer API endpoints
17-
export const LOCAL_EXPLORER_API_PATH = `${LOCAL_EXPLORER_BASE_PATH}/api`;
18+
export const LOCAL_EXPLORER_API_PATH = `${CorePaths.EXPLORER}/api`;
1819
// Service prefix for all regular user workers
1920
const SERVICE_USER_PREFIX = `${CORE_PLUGIN_NAME}:user`;
2021
// Service prefix for `workerd`'s builtin services (network, external, disk)

packages/miniflare/src/plugins/core/proxy/client.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { prefixStream, readPrefix } from "../../../shared";
1010
import {
1111
Awaitable,
1212
CoreHeaders,
13+
CorePaths,
1314
createHTTPReducers,
1415
createHTTPRevivers,
1516
isDurableObjectStub,
@@ -97,7 +98,10 @@ export class ProxyClient {
9798
#bridge: ProxyClientBridge;
9899

99100
constructor(runtimeEntryURL: URL, dispatchFetch: DispatchFetch) {
100-
this.#bridge = new ProxyClientBridge(runtimeEntryURL, dispatchFetch);
101+
this.#bridge = new ProxyClientBridge(
102+
new URL(CorePaths.PLATFORM_PROXY, runtimeEntryURL),
103+
dispatchFetch
104+
);
101105
}
102106

103107
// Lazily initialise proxies as required
@@ -120,7 +124,7 @@ export class ProxyClient {
120124
setRuntimeEntryURL(runtimeEntryURL: URL) {
121125
// This function will be called whenever the runtime restarts. The URL may
122126
// be different if the port has changed.
123-
this.#bridge.url = runtimeEntryURL;
127+
this.#bridge.url = new URL(CorePaths.PLATFORM_PROXY, runtimeEntryURL);
124128
}
125129

126130
dispose(): Promise<void> {
@@ -720,9 +724,12 @@ class ProxyStubHandler<T extends object>
720724
#fetcherFetchCall(args: unknown[]) {
721725
// @ts-expect-error `...args` isn't type-safe here, but `undici` should
722726
// validate types at runtime, and throw appropriate errors
723-
const request = new Request(...args);
727+
const userRequest = new Request(...args);
728+
// Create a new request with the proxy URL, preserving the original request
729+
const request = new Request(this.bridge.url, userRequest);
724730
// If adding new headers here, remember to `delete()` them in `ProxyServer`
725731
// before calling `fetch()`.
732+
request.headers.set(CoreHeaders.OP_ORIGINAL_URL, userRequest.url);
726733
request.headers.set(CoreHeaders.OP_SECRET, PROXY_SECRET_HEX);
727734
request.headers.set(CoreHeaders.OP, ProxyOps.CALL);
728735
request.headers.set(CoreHeaders.OP_TARGET, this.#stringifiedTarget);

packages/miniflare/src/plugins/core/proxy/fetch-sync.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,18 @@ let dispatcher;
5050
5151
port.addEventListener("message", async (event) => {
5252
const { id, method, url, headers, body } = event.data;
53-
if (dispatcherUrl !== url) {
54-
dispatcherUrl = url;
55-
dispatcher = new Pool(url, {
56-
connect: { rejectUnauthorized: false },
57-
// Disable timeouts for local dev — long-running responses (streaming,
53+
try {
54+
if (dispatcherUrl !== url) {
55+
dispatcherUrl = url;
56+
dispatcher = new Pool(new URL(url).origin, {
57+
connect: { rejectUnauthorized: false },
58+
// Disable timeouts for local dev — long-running responses (streaming,
5859
// slow uploads, long-polling) should not be killed by undici defaults.
5960
headersTimeout: 0,
6061
bodyTimeout: 0,
61-
});
62-
}
63-
headers["${CoreHeaders.OP_SYNC}"] = "true";
64-
try {
62+
});
63+
}
64+
headers["${CoreHeaders.OP_SYNC}"] = "true";
6565
// body cannot be a ReadableStream, so no need to specify duplex
6666
const response = await fetch(url, { method, headers, body, dispatcher });
6767
const responseBody = response.headers.get("${CoreHeaders.OP_RESULT_TYPE}") === "ReadableStream"
@@ -86,9 +86,10 @@ port.addEventListener("message", async (event) => {
8686
// If error failed to serialise, post simplified version
8787
port.postMessage({ id, error: new Error(String(error)) });
8888
}
89+
} finally {
90+
Atomics.store(notifyHandle, /* index */ 0, /* value */ 1);
91+
Atomics.notify(notifyHandle, /* index */ 0);
8992
}
90-
Atomics.store(notifyHandle, /* index */ 0, /* value */ 1);
91-
Atomics.notify(notifyHandle, /* index */ 0);
9293
});
9394
9495
port.start();

packages/miniflare/src/workers/core/constants.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
/**
2+
* Reserved `/cdn-cgi/` paths for internal Miniflare endpoints.
3+
* These paths are reserved by Cloudflare's network and won't conflict with user routes.
4+
*/
5+
export const CorePaths = {
6+
/** Magic proxy used by getPlatformProxy */
7+
PLATFORM_PROXY: "/cdn-cgi/platform-proxy",
8+
/** Trigger scheduled event handlers */
9+
SCHEDULED: "/cdn-cgi/handler/scheduled",
10+
/** Trigger email event handlers */
11+
EMAIL: "/cdn-cgi/handler/email",
12+
/** Handler path prefix for validation */
13+
HANDLER_PREFIX: "/cdn-cgi/handler/",
14+
/** Live reload WebSocket endpoint */
15+
LIVE_RELOAD: "/cdn-cgi/mf/reload",
16+
/** Local explorer UI and API */
17+
EXPLORER: "/cdn-cgi/explorer",
18+
/** Legacy way to trigger scheduled event handlers */
19+
LEGACY_SCHEDULED: "/cdn-cgi/mf/scheduled",
20+
} as const;
21+
122
export const CoreHeaders = {
223
CUSTOM_FETCH_SERVICE: "MF-Custom-Fetch-Service",
324
CUSTOM_NODE_SERVICE: "MF-Custom-Node-Service",
@@ -25,6 +46,7 @@ export const CoreHeaders = {
2546
OP_SYNC: "MF-Op-Sync",
2647
OP_STRINGIFIED_SIZE: "MF-Op-Stringified-Size",
2748
OP_RESULT_TYPE: "MF-Op-Result-Type",
49+
OP_ORIGINAL_URL: "MF-Op-Original-URL",
2850
} as const;
2951

3052
export const CoreBindings = {

packages/miniflare/src/workers/core/entry.worker.ts

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ import {
99
yellow,
1010
} from "kleur/colors";
1111
import { HttpError, LogLevel, SharedHeaders } from "miniflare:shared";
12-
import { LOCAL_EXPLORER_BASE_PATH } from "../../plugins/core/constants";
1312
import { isCompressedByCloudflareFL } from "../../shared/mime-types";
14-
import { CoreBindings, CoreHeaders } from "./constants";
13+
import { CoreBindings, CoreHeaders, CorePaths } from "./constants";
1514
import { handleEmail } from "./email";
1615
import { STATUS_CODES } from "./http";
1716
import { matchRoutes, WorkerRoute } from "./routing";
@@ -469,9 +468,14 @@ export default <ExportedHandler<Env>>{
469468
};
470469
request = new Request(request, { cf });
471470

472-
// The magic proxy client (used by getPlatformProxy) will always specify an operation
473-
const isProxy = request.headers.get(CoreHeaders.OP) !== null;
474-
if (isProxy) return handleProxy(request, env);
471+
// The magic proxy client (used by getPlatformProxy)
472+
if (new URL(request.url).pathname === CorePaths.PLATFORM_PROXY) {
473+
if (request.headers.get(CoreHeaders.OP) !== null) {
474+
return handleProxy(request, env);
475+
}
476+
477+
return new Response("Invalid proxy request", { status: 400 });
478+
}
475479

476480
// `dispatchFetch()` will always inject this header. When
477481
// calling this function, we never want to display the pretty-error page.
@@ -487,8 +491,8 @@ export default <ExportedHandler<Env>>{
487491
if (env[CoreBindings.SERVICE_LOCAL_EXPLORER]) {
488492
const preRewriteUrl = new URL(request.url);
489493
if (
490-
preRewriteUrl.pathname === LOCAL_EXPLORER_BASE_PATH ||
491-
preRewriteUrl.pathname.startsWith(`${LOCAL_EXPLORER_BASE_PATH}/`)
494+
preRewriteUrl.pathname === CorePaths.EXPLORER ||
495+
preRewriteUrl.pathname.startsWith(`${CorePaths.EXPLORER}/`)
492496
) {
493497
validateLocalExplorerRequest(
494498
request,
@@ -514,18 +518,18 @@ export default <ExportedHandler<Env>>{
514518
try {
515519
if (env[CoreBindings.SERVICE_LOCAL_EXPLORER]) {
516520
if (
517-
url.pathname === LOCAL_EXPLORER_BASE_PATH ||
518-
url.pathname.startsWith(`${LOCAL_EXPLORER_BASE_PATH}/`)
521+
url.pathname === CorePaths.EXPLORER ||
522+
url.pathname.startsWith(`${CorePaths.EXPLORER}/`)
519523
) {
520524
return await env[CoreBindings.SERVICE_LOCAL_EXPLORER].fetch(request);
521525
}
522526
}
523527
if (env[CoreBindings.TRIGGER_HANDLERS]) {
524528
if (
525-
url.pathname === "/cdn-cgi/handler/scheduled" ||
526-
/* legacy URL path */ url.pathname === "/cdn-cgi/mf/scheduled"
529+
url.pathname === CorePaths.SCHEDULED ||
530+
/* legacy URL path */ url.pathname === CorePaths.LEGACY_SCHEDULED
527531
) {
528-
if (url.pathname === "/cdn-cgi/mf/scheduled") {
532+
if (url.pathname === CorePaths.LEGACY_SCHEDULED) {
529533
ctx.waitUntil(
530534
env[CoreBindings.SERVICE_LOOPBACK].fetch(
531535
"http://localhost/core/log",
@@ -534,15 +538,15 @@ export default <ExportedHandler<Env>>{
534538
headers: {
535539
[SharedHeaders.LOG_LEVEL]: LogLevel.WARN.toString(),
536540
},
537-
body: `Triggering scheduled handlers via a request to \`/cdn-cgi/mf/scheduled\` is deprecated, and will be removed in a future version of Miniflare. Instead, send a request to \`/cdn-cgi/handler/scheduled\``,
541+
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}\``,
538542
}
539543
)
540544
);
541545
}
542546
return await handleScheduled(url.searchParams, service);
543547
}
544548

545-
if (url.pathname === "/cdn-cgi/handler/email") {
549+
if (url.pathname === CorePaths.EMAIL) {
546550
return await handleEmail(
547551
url.searchParams,
548552
request,
@@ -552,9 +556,9 @@ export default <ExportedHandler<Env>>{
552556
);
553557
}
554558

555-
if (url.pathname.startsWith("/cdn-cgi/handler/")) {
559+
if (url.pathname.startsWith(CorePaths.HANDLER_PREFIX)) {
556560
return new Response(
557-
`"${url.pathname}" is not a valid handler. Did you mean to use "/cdn-cgi/handler/scheduled" or "/cdn-cgi/handler/email"?`,
561+
`"${url.pathname}" is not a valid handler. Did you mean to use "${CorePaths.SCHEDULED}" or "${CorePaths.EMAIL}"?`,
558562
{ status: 404 }
559563
);
560564
}

packages/miniflare/src/workers/core/proxy.worker.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,14 +270,17 @@ export class ProxyServer implements DurableObject {
270270

271271
// See `isFetcherFetch()` comment for why this special
272272
if (isFetcherFetch(targetName, keyHeader)) {
273-
const originalUrl = request.headers.get(CoreHeaders.ORIGINAL_URL);
273+
const originalUrl =
274+
request.headers.get(CoreHeaders.OP_ORIGINAL_URL) ??
275+
request.headers.get(CoreHeaders.ORIGINAL_URL);
274276
const url = new URL(originalUrl ?? request.url);
275277
// Create a new request to allow header mutation and use original URL
276278
request = new Request(url, request);
277279
request.headers.delete(CoreHeaders.OP_SECRET);
278280
request.headers.delete(CoreHeaders.OP);
279281
request.headers.delete(CoreHeaders.OP_TARGET);
280282
request.headers.delete(CoreHeaders.OP_KEY);
283+
request.headers.delete(CoreHeaders.OP_ORIGINAL_URL);
281284
request.headers.delete(CoreHeaders.ORIGINAL_URL);
282285
request.headers.delete(CoreHeaders.DISABLE_PRETTY_ERROR);
283286
return func.call(target, request);

packages/miniflare/test/plugins/core/proxy/client.spec.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { text } from "node:stream/consumers";
55
import { ReadableStream, WritableStream } from "node:stream/web";
66
import util from "node:util";
77
import {
8+
CorePaths,
89
DeferredPromise,
910
fetch,
1011
MessageEvent,
@@ -56,6 +57,28 @@ describe("ProxyClient", () => {
5657
expect(event.data).toBe("echo:hello");
5758
});
5859

60+
test("preserves original URL for service binding fetch", async ({
61+
expect,
62+
}) => {
63+
const mf = new Miniflare({
64+
script: nullScript,
65+
serviceBindings: {
66+
CUSTOM(request: Request) {
67+
return new Response(request.url);
68+
},
69+
},
70+
});
71+
useDispose(mf);
72+
73+
const { CUSTOM } = await mf.getBindings<{
74+
CUSTOM: ReplaceWorkersTypes<Fetcher>;
75+
}>();
76+
77+
const url = "https://placeholder/path?query=value";
78+
const res = await CUSTOM.fetch(url);
79+
expect(await res.text()).toBe(url);
80+
});
81+
5982
test("supports serialising multiple ReadableStreams, Blobs and Files", async ({
6083
expect,
6184
}) => {
@@ -329,32 +352,38 @@ describe("ProxyClient", () => {
329352
const mf = new Miniflare({ script: nullScript });
330353
useDispose(mf);
331354
const url = await mf.ready;
355+
const proxyUrl = new URL(CorePaths.PLATFORM_PROXY, url);
332356

333357
// Check validates `Host` header
334358
const statusPromise = new DeferredPromise<number>();
335359
const req = http.get(
336-
url,
360+
proxyUrl,
337361
{ setHost: false, headers: { "MF-Op": "GET", Host: "localhost" } },
338362
(res) => statusPromise.resolve(res.statusCode ?? 0)
339363
);
340364
req.on("error", (error) => statusPromise.reject(error));
341365
expect(await statusPromise).toBe(401);
342366

343367
// Check validates `MF-Op-Secret` header
344-
let res = await fetch(url, {
368+
let res = await fetch(proxyUrl, {
345369
headers: { "MF-Op": "GET" }, // (missing)
346370
});
347371
expect(res.status).toBe(401);
348372
await res.arrayBuffer(); // (drain)
349-
res = await fetch(url, {
373+
res = await fetch(proxyUrl, {
350374
headers: { "MF-Op": "GET", "MF-Op-Secret": "aaaa" }, // (too short)
351375
});
352376
expect(res.status).toBe(401);
353377
await res.arrayBuffer(); // (drain)
354-
res = await fetch(url, {
378+
res = await fetch(proxyUrl, {
355379
headers: { "MF-Op": "GET", "MF-Op-Secret": "a".repeat(32) }, // (wrong)
356380
});
357381
expect(res.status).toBe(401);
358382
await res.arrayBuffer(); // (drain)
383+
384+
// Check requests to proxy path without MF-Op header return 400
385+
res = await fetch(proxyUrl);
386+
expect(res.status).toBe(400);
387+
await res.arrayBuffer(); // (drain)
359388
});
360389
});

0 commit comments

Comments
 (0)