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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/quiet-routers-loop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@cloudflare/workers-shared": patch
"miniflare": patch
"@cloudflare/vite-plugin": patch
---

Bypass the router Worker loopback on the normal request path

The inner routing entrypoint is now the default, avoiding the latency added by forwarding every request through `ctx.exports`. The outer loopback entrypoint and its supporting infrastructure remain available as named exports in the router Worker, Miniflare, and Vite plugin bundles so the boundary can be re-enabled later.
Comment on lines +7 to +9

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changeset description contains implementation details instead of user-facing impact

REVIEW.md states: "Changesets should target users of the tools (e.g. Wrangler users) rather than maintainers. Avoid including implementation details like 'moves X from hybridModules to nativeModules' or 'removes polyfill implementation' or 'adds comprehensive tests'. Instead, focus on user-facing impact and benefits." The changeset title "Bypass the router Worker loopback on the normal request path" and body mentioning "inner routing entrypoint", "ctx.exports", "outer loopback entrypoint", and "named exports in the router Worker, Miniflare, and Vite plugin bundles" are all implementation details. The user-facing benefit (reduced latency / improved performance) should be the focus instead.

Suggested change
Bypass the router Worker loopback on the normal request path
The inner routing entrypoint is now the default, avoiding the latency added by forwarding every request through `ctx.exports`. The outer loopback entrypoint and its supporting infrastructure remain available as named exports in the router Worker, Miniflare, and Vite plugin bundles so the boundary can be re-enabled later.
Improve routing performance for Workers with assets
Reduce request handling latency by streamlining the router Worker's request path. The loopback infrastructure remains available for future use.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

1 change: 1 addition & 0 deletions packages/miniflare/src/workers/assets/router.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
export {
default,
RouterInnerEntrypoint,
RouterOuterEntrypoint,
} from "@cloudflare/workers-shared/router-worker";
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export {
default,
RouterInnerEntrypoint,
RouterOuterEntrypoint,
} from "@cloudflare/workers-shared/router-worker";
55 changes: 33 additions & 22 deletions packages/workers-shared/router-worker/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,44 +45,53 @@ type LoopbackExecutionContext = ExecutionContext & {
};
};

export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
// This now-unused Entrypoint can be used for loopback invocations if made the
// default export, which is how cohorted deployments will be implemented.
// For now, the latency added by loopback invocations is too much for such a
// load-bearing Worker. When that has been addressed in the future, re-enable
// loopback by making this the default export.
export class RouterOuterEntrypoint extends WorkerEntrypoint<Env> {
override async fetch(request: Request): Promise<Response> {
// Router worker is typechecked both in this package and as a transitive
// dependency during miniflare builds. In the miniflare type context,
// Cloudflare.Exports may not include this worker's mainModule mapping from
// worker-configuration.d.ts, so `ctx.exports.RouterInnerEntrypoint` does not
// resolve structurally. Keep this narrow cast so loopback typechecks in both
// compilation contexts without changing runtime behavior.
const loopbackCtx = ctx as LoopbackExecutionContext;
const loopbackCtx = this.ctx as LoopbackExecutionContext;

const analytics = new Analytics(env.ANALYTICS);
const analytics = new Analytics(this.env.ANALYTICS);
let inner;
try {
if (env.COLO_METADATA && env.VERSION_METADATA && env.CONFIG) {
if (
this.env.COLO_METADATA &&
this.env.VERSION_METADATA &&
this.env.CONFIG
) {
const url = new URL(request.url);
analytics.setData({
accountId: env.CONFIG.account_id,
scriptId: env.CONFIG.script_id,
coloId: env.COLO_METADATA.coloId,
metalId: env.COLO_METADATA.metalId,
coloTier: env.COLO_METADATA.coloTier,
coloRegion: env.COLO_METADATA.coloRegion,
accountId: this.env.CONFIG.account_id,
scriptId: this.env.CONFIG.script_id,
coloId: this.env.COLO_METADATA.coloId,
metalId: this.env.COLO_METADATA.metalId,
coloTier: this.env.COLO_METADATA.coloTier,
coloRegion: this.env.COLO_METADATA.coloRegion,
hostname: url.hostname,
version: env.VERSION_METADATA.tag,
version: this.env.VERSION_METADATA.tag,
});
}
inner = loopbackCtx.exports.RouterInnerEntrypoint({ props: {} });
} catch (err) {
const sentry = setupSentry(
request,
ctx,
env.SENTRY_DSN,
env.SENTRY_ACCESS_CLIENT_ID,
env.SENTRY_ACCESS_CLIENT_SECRET,
env.COLO_METADATA,
env.VERSION_METADATA,
env.CONFIG?.account_id,
env.CONFIG?.script_id
this.ctx,
this.env.SENTRY_DSN,
this.env.SENTRY_ACCESS_CLIENT_ID,
this.env.SENTRY_ACCESS_CLIENT_SECRET,
this.env.COLO_METADATA,
this.env.VERSION_METADATA,
this.env.CONFIG?.account_id,
this.env.CONFIG?.script_id
);
sentry?.captureException(err);

Expand All @@ -95,8 +104,8 @@ export default {
}

return inner.fetch(request);
},
};
}
}

export class RouterInnerEntrypoint extends WorkerEntrypoint<Env> {
override async fetch(request: Request): Promise<Response> {
Expand Down Expand Up @@ -355,6 +364,8 @@ export class RouterInnerEntrypoint extends WorkerEntrypoint<Env> {
}
}

export default RouterInnerEntrypoint;

/**
* Check if the Content Type is allowed for the the `_next/image` endpoint.
*
Expand Down
43 changes: 37 additions & 6 deletions packages/workers-shared/router-worker/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ import {
createExecutionContext,
env as runtimeEnv,
} from "cloudflare:test";
import { describe, it } from "vitest";
import { RouterInnerEntrypoint } from "../src/worker";
import { describe, it, vi } from "vitest";
import DefaultRouterEntrypoint, {
RouterInnerEntrypoint,
RouterOuterEntrypoint,
} from "../src/worker";
import type { Env } from "../src/worker";

async function fetchFromInnerEntrypoint(
Expand All @@ -15,16 +18,20 @@ async function fetchFromInnerEntrypoint(
return new RouterInnerEntrypoint(ctx, env).fetch(request);
}

describe("runtime loopback", () => {
it("routes through outer->inner loopback at runtime boundary", async ({
describe("entrypoints", () => {
it("uses the inner entrypoint as the default export", ({ expect }) => {
expect(DefaultRouterEntrypoint).toBe(RouterInnerEntrypoint);
});

it("routes directly through the inner entrypoint at the runtime boundary", async ({
expect,
}) => {
(runtimeEnv as Env).CONFIG = {
has_user_worker: false,
};
(runtimeEnv as Env).ASSET_WORKER = {
async fetch(_request: Request): Promise<Response> {
return new Response("loopback asset worker");
return new Response("asset worker");
},
async unstable_canFetch(_request: Request): Promise<boolean> {
return true;
Expand All @@ -34,7 +41,31 @@ describe("runtime loopback", () => {
const response = await SELF.fetch("https://example.com");

expect(response.status).toBe(200);
expect(await response.text()).toBe("loopback asset worker");
expect(await response.text()).toBe("asset worker");
});

it("keeps the outer-to-inner loopback available", async ({ expect }) => {
const request = new Request("https://example.com");
const ctx = createExecutionContext();
const innerFetch = vi.fn(async (_request: Request) => {
return new Response("inner response");
});
const createInnerEntrypoint = vi.fn(() => ({ fetch: innerFetch }));
Object.defineProperty(ctx, "exports", {
value: {
RouterInnerEntrypoint: createInnerEntrypoint,
},
});

const response = await new RouterOuterEntrypoint(ctx, {} as Env).fetch(
request
);

expect(createInnerEntrypoint).toHaveBeenCalledOnce();
expect(createInnerEntrypoint).toHaveBeenCalledWith({ props: {} });
expect(innerFetch).toHaveBeenCalledOnce();
expect(innerFetch).toHaveBeenCalledWith(request);
expect(await response.text()).toBe("inner response");
});
});

Expand Down
Loading