From 13a8aea483abccf3da59ed0ec469c6e1a5f9281d Mon Sep 17 00:00:00 2001 From: Matthew Rodgers Date: Fri, 23 May 2025 14:51:07 -0700 Subject: [PATCH 1/5] WC-3584 Refactor routing to asset or user worker Centralizing some of these pieces will make it easier to add free tier limiting --- .../router-worker/src/worker.ts | 181 +++++++----------- 1 file changed, 72 insertions(+), 109 deletions(-) diff --git a/packages/workers-shared/router-worker/src/worker.ts b/packages/workers-shared/router-worker/src/worker.ts index f053608030c0..8f1be15879e7 100644 --- a/packages/workers-shared/router-worker/src/worker.ts +++ b/packages/workers-shared/router-worker/src/worker.ts @@ -77,23 +77,74 @@ export default { const maybeSecondRequest = request.clone(); - let shouldBlockNonImageResponse = false; - if (url.pathname === "/_next/image") { - // is a next image - const queryURLParam = url.searchParams.get("url"); - if (queryURLParam && !queryURLParam.startsWith("/")) { - // that's a remote resource - if ( - maybeSecondRequest.method !== "GET" || - maybeSecondRequest.headers.get("sec-fetch-dest") !== "image" - ) { - // that was not loaded via a browser's tag - shouldBlockNonImageResponse = true; - analytics.setData({ abuseMitigationURLHost: queryURLParam }); - } - // otherwise, we're good + const routeToUserWorker = async ({ + asset, + }: { + asset: "static_routing" | "none"; + }) => { + if (!config.has_user_worker) { + throw new Error( + "Fetch for user worker without having a user worker binding" + ); } - } + analytics.setData({ dispatchtype: DISPATCH_TYPE.WORKER }); + userWorkerInvocation = true; + return env.JAEGER.enterSpan("dispatch_worker", async (span) => { + span.setTags({ + hasUserWorker: true, + asset: asset, + dispatchType: DISPATCH_TYPE.WORKER, + }); + + let shouldBlockNonImageResponse = false; + if (url.pathname === "/_next/image") { + // is a next image + const queryURLParam = url.searchParams.get("url"); + if (queryURLParam && !queryURLParam.startsWith("/")) { + // that's a remote resource + if ( + maybeSecondRequest.method !== "GET" || + maybeSecondRequest.headers.get("sec-fetch-dest") !== "image" + ) { + // that was not loaded via a browser's tag + shouldBlockNonImageResponse = true; + analytics.setData({ abuseMitigationURLHost: queryURLParam }); + } + // otherwise, we're good + } + } + + if (shouldBlockNonImageResponse) { + const resp = await env.USER_WORKER.fetch(maybeSecondRequest); + if ( + !resp.headers.get("content-type")?.startsWith("image/") && + resp.status !== 304 + ) { + analytics.setData({ abuseMitigationBlocked: true }); + return new Response("Blocked", { status: 403 }); + } + return resp; + } + return env.USER_WORKER.fetch(maybeSecondRequest); + }); + }; + + const routeToAssets = async ({ + asset, + }: { + asset: "static_routing" | "found" | "none"; + }) => { + analytics.setData({ dispatchtype: DISPATCH_TYPE.ASSETS }); + return await env.JAEGER.enterSpan("dispatch_assets", async (span) => { + span.setTags({ + hasUserWorker: config.has_user_worker, + asset: asset, + dispatchType: DISPATCH_TYPE.ASSETS, + }); + + return env.ASSET_WORKER.fetch(maybeSecondRequest); + }); + }; if (config.static_routing) { // evaluate "exclude" rules @@ -107,18 +158,9 @@ export default { ) { // direct to asset worker analytics.setData({ - dispatchtype: DISPATCH_TYPE.ASSETS, staticRoutingDecision: STATIC_ROUTING_DECISION.ROUTED, }); - return await env.JAEGER.enterSpan("dispatch_assets", async (span) => { - span.setTags({ - hasUserWorker: config.has_user_worker, - asset: "static_routing", - dispatchType: DISPATCH_TYPE.ASSETS, - }); - - return env.ASSET_WORKER.fetch(maybeSecondRequest); - }); + return await routeToAssets({ asset: "static_routing" }); } // evaluate "include" rules const includeRulesMatcher = generateStaticRoutingRuleMatcher( @@ -136,30 +178,9 @@ export default { } // direct to user worker analytics.setData({ - dispatchtype: DISPATCH_TYPE.WORKER, staticRoutingDecision: STATIC_ROUTING_DECISION.ROUTED, }); - return await env.JAEGER.enterSpan("dispatch_worker", async (span) => { - span.setTags({ - hasUserWorker: true, - asset: "static_routing", - dispatchType: DISPATCH_TYPE.WORKER, - }); - - userWorkerInvocation = true; - if (shouldBlockNonImageResponse) { - const resp = await env.USER_WORKER.fetch(maybeSecondRequest); - if ( - !resp.headers.get("content-type")?.startsWith("image/") && - resp.status !== 304 - ) { - analytics.setData({ abuseMitigationBlocked: true }); - return new Response("Blocked", { status: 403 }); - } - return resp; - } - return env.USER_WORKER.fetch(maybeSecondRequest); - }); + return await routeToUserWorker({ asset: "static_routing" }); } analytics.setData({ @@ -172,75 +193,17 @@ export default { // User's configuration indicates they want user-Worker to run ahead of any // assets. Do not provide any fallback logic. if (config.invoke_user_worker_ahead_of_assets) { - if (!config.has_user_worker) { - throw new Error( - "Fetch for user worker without having a user worker binding" - ); - } - - analytics.setData({ dispatchtype: DISPATCH_TYPE.WORKER }); - return await env.JAEGER.enterSpan("dispatch_worker", async (span) => { - span.setTags({ - hasUserWorker: true, - asset: "ignored", - dispatchType: DISPATCH_TYPE.WORKER, - }); - - userWorkerInvocation = true; - if (shouldBlockNonImageResponse) { - const resp = await env.USER_WORKER.fetch(maybeSecondRequest); - if ( - !resp.headers.get("content-type")?.startsWith("image/") && - resp.status !== 304 - ) { - analytics.setData({ abuseMitigationBlocked: true }); - return new Response("Blocked", { status: 403 }); - } - return resp; - } - return env.USER_WORKER.fetch(maybeSecondRequest); - }); + return await routeToUserWorker({ asset: "static_routing" }); } // If we have a user-Worker, but no assets, dispatch to Worker script const assetsExist = await env.ASSET_WORKER.unstable_canFetch(request); if (config.has_user_worker && !assetsExist) { - analytics.setData({ dispatchtype: DISPATCH_TYPE.WORKER }); - - return await env.JAEGER.enterSpan("dispatch_worker", async (span) => { - span.setTags({ - hasUserWorker: config.has_user_worker, - asset: assetsExist, - dispatchType: DISPATCH_TYPE.WORKER, - }); - - userWorkerInvocation = true; - if (shouldBlockNonImageResponse) { - const resp = await env.USER_WORKER.fetch(maybeSecondRequest); - if ( - !resp.headers.get("content-type")?.startsWith("image/") && - resp.status !== 304 - ) { - analytics.setData({ abuseMitigationBlocked: true }); - return new Response("Blocked", { status: 403 }); - } - return resp; - } - return env.USER_WORKER.fetch(maybeSecondRequest); - }); + return await routeToUserWorker({ asset: "none" }); } // Otherwise, we either don't have a user worker, OR we have matching assets and should fetch from the assets binding - analytics.setData({ dispatchtype: DISPATCH_TYPE.ASSETS }); - return await env.JAEGER.enterSpan("dispatch_assets", async (span) => { - span.setTags({ - hasUserWorker: config.has_user_worker, - asset: assetsExist, - dispatchType: DISPATCH_TYPE.ASSETS, - }); - - return env.ASSET_WORKER.fetch(maybeSecondRequest); - }); + return await routeToAssets({ asset: assetsExist ? "found" : "none" }); } catch (err) { if (userWorkerInvocation) { // Don't send user Worker errors to sentry; we have no way to distinguish between From 5305c9b106f709cec98045058ee1c532fd055a38 Mon Sep 17 00:00:00 2001 From: Matthew Rodgers Date: Fri, 23 May 2025 15:22:02 -0700 Subject: [PATCH 2/5] WC-3584 Add eyeball routing config Provided via a new optional param binding. When provided, it contains info about free tier limiting for this account. --- .../router-worker/src/configuration.ts | 14 ++++++++++++-- .../workers-shared/router-worker/src/worker.ts | 10 ++++++++-- .../workers-shared/router-worker/wrangler.jsonc | 6 ++++++ packages/workers-shared/utils/types.ts | 8 ++++++++ 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/packages/workers-shared/router-worker/src/configuration.ts b/packages/workers-shared/router-worker/src/configuration.ts index 3dde3943505d..2c44712dac58 100644 --- a/packages/workers-shared/router-worker/src/configuration.ts +++ b/packages/workers-shared/router-worker/src/configuration.ts @@ -1,6 +1,8 @@ -import type { RouterConfig } from "../../utils/types"; +import type { EyeballRouterConfig, RouterConfig } from "../../utils/types"; -export const applyConfigurationDefaults = ( +type RequiredEyeballRouterConfig = Required>; + +export const applyRouterConfigDefaults = ( configuration?: RouterConfig ): Required => { return { @@ -15,3 +17,11 @@ export const applyConfigurationDefaults = ( }, }; }; + +export const applyEyeballConfigDefaults = ( + eyeballConfiguration?: EyeballRouterConfig +): RequiredEyeballRouterConfig => { + return { + limitedAssetsOnly: eyeballConfiguration?.limitedAssetsOnly ?? false, + }; +}; diff --git a/packages/workers-shared/router-worker/src/worker.ts b/packages/workers-shared/router-worker/src/worker.ts index 8f1be15879e7..c9aff1b4d347 100644 --- a/packages/workers-shared/router-worker/src/worker.ts +++ b/packages/workers-shared/router-worker/src/worker.ts @@ -3,9 +3,13 @@ import { PerformanceTimer } from "../../utils/performance"; import { setupSentry } from "../../utils/sentry"; import { mockJaegerBinding } from "../../utils/tracing"; import { Analytics, DISPATCH_TYPE, STATIC_ROUTING_DECISION } from "./analytics"; -import { applyConfigurationDefaults } from "./configuration"; +import { + applyEyeballConfigDefaults, + applyRouterConfigDefaults, +} from "./configuration"; import type AssetWorker from "../../asset-worker"; import type { + EyeballRouterConfig, JaegerTracing, RouterConfig, UnsafePerformanceTimer, @@ -16,6 +20,7 @@ export interface Env { ASSET_WORKER: Service; USER_WORKER: Fetcher; CONFIG: RouterConfig; + EYEBALL_CONFIG: EyeballRouterConfig; SENTRY_DSN: string; ENVIRONMENT: Environment; @@ -55,7 +60,8 @@ export default { ); const hasStaticRouting = env.CONFIG.static_routing !== undefined; - const config = applyConfigurationDefaults(env.CONFIG); + const config = applyRouterConfigDefaults(env.CONFIG); + const eyeballConfig = applyEyeballConfigDefaults(env.EYEBALL_CONFIG); const url = new URL(request.url); diff --git a/packages/workers-shared/router-worker/wrangler.jsonc b/packages/workers-shared/router-worker/wrangler.jsonc index 33d62792427e..e89f322ed55d 100644 --- a/packages/workers-shared/router-worker/wrangler.jsonc +++ b/packages/workers-shared/router-worker/wrangler.jsonc @@ -25,6 +25,12 @@ "type": "param", "param": "routerConfig", }, + { + "name": "EYEBALL_CONFIG", + "type": "param", + "param": "eyeballRouterConfig", + "optional": true, + }, { "name": "ASSET_WORKER", "type": "internal_assets", diff --git a/packages/workers-shared/utils/types.ts b/packages/workers-shared/utils/types.ts index 9bcfefdb9c30..1cd4374b3c11 100644 --- a/packages/workers-shared/utils/types.ts +++ b/packages/workers-shared/utils/types.ts @@ -20,6 +20,13 @@ export const RouterConfigSchema = z.object({ ...InternalConfigSchema.shape, }); +export const EyeballRouterConfigSchema = z.union([ + z.object({ + limitedAssetsOnly: z.boolean().optional(), + }), + z.null(), +]); + const MetadataStaticRedirectEntry = z.object({ status: z.number(), to: z.string(), @@ -79,6 +86,7 @@ export const AssetConfigSchema = z.object({ ...InternalConfigSchema.shape, }); +export type EyeballRouterConfig = z.infer; export type RouterConfig = z.infer; export type AssetConfig = z.infer; From 28e38899b3abdd6484fc52a1b0423f9219eafd49 Mon Sep 17 00:00:00 2001 From: Matthew Rodgers Date: Fri, 23 May 2025 15:23:47 -0700 Subject: [PATCH 3/5] WC-3584 Deny requests to the user worker when over free tier limits Returns an html page (included as direct module, via esbuild) with a 429 status code, mirroring the current free tier limiting behavior. It also sets a new field in our analytics so we can observe free tier invocation denials. This page can evolve as we need it to, this is just my first pass at the response. --- .../router-worker/src/analytics.ts | 3 + .../router-worker/src/limited-response.html | 21 +++ .../router-worker/src/worker.ts | 10 ++ .../router-worker/tests/index.test.ts | 159 ++++++++++++++++++ .../workers-shared/router-worker/types.d.ts | 4 + 5 files changed, 197 insertions(+) create mode 100644 packages/workers-shared/router-worker/src/limited-response.html create mode 100644 packages/workers-shared/router-worker/types.d.ts diff --git a/packages/workers-shared/router-worker/src/analytics.ts b/packages/workers-shared/router-worker/src/analytics.ts index 2d7d7c3e7c3e..e14f82ee12f5 100644 --- a/packages/workers-shared/router-worker/src/analytics.ts +++ b/packages/workers-shared/router-worker/src/analytics.ts @@ -35,6 +35,8 @@ type Data = { staticRoutingDecision?: STATIC_ROUTING_DECISION; // double7 - Whether the request was blocked by abuse mitigation or not abuseMitigationBlocked?: boolean; + // double8 - User worker invocation denied due to free tier limiting + userWorkerFreeTierLimiting?: boolean; // -- Blobs -- // blob1 - Hostname of the request @@ -93,6 +95,7 @@ export class Analytics { : Number(this.data.userWorkerAhead), this.data.staticRoutingDecision ?? STATIC_ROUTING_DECISION.NOT_PROVIDED, // double6 this.data.abuseMitigationBlocked ? 1 : 0, // double7 + this.data.userWorkerFreeTierLimiting ? 1 : 0, // double8 ], blobs: [ this.data.hostname?.substring(0, 256), // blob1 - trim to 256 bytes diff --git a/packages/workers-shared/router-worker/src/limited-response.html b/packages/workers-shared/router-worker/src/limited-response.html new file mode 100644 index 000000000000..0e851e10c98b --- /dev/null +++ b/packages/workers-shared/router-worker/src/limited-response.html @@ -0,0 +1,21 @@ + + + This website is temporarily limited + + +

This website has been temporarily rate limited

+

+ You cannot access this site because the owner has reached their plan + limits. Check back later once traffic has gone down. +

+

+ If you are owner of this website, prevent this from happening again by + upgrading your plan on the + Cloudflare Workers dashboard. +

+ + diff --git a/packages/workers-shared/router-worker/src/worker.ts b/packages/workers-shared/router-worker/src/worker.ts index c9aff1b4d347..6a672f524a5d 100644 --- a/packages/workers-shared/router-worker/src/worker.ts +++ b/packages/workers-shared/router-worker/src/worker.ts @@ -7,6 +7,7 @@ import { applyEyeballConfigDefaults, applyRouterConfigDefaults, } from "./configuration"; +import limitedResponse from "./limited-response.html"; import type AssetWorker from "../../asset-worker"; import type { EyeballRouterConfig, @@ -93,6 +94,15 @@ export default { "Fetch for user worker without having a user worker binding" ); } + if (eyeballConfig.limitedAssetsOnly) { + analytics.setData({ userWorkerFreeTierLimiting: true }); + return new Response(limitedResponse, { + status: 429, + headers: { + "Content-Type": "text/html", + }, + }); + } analytics.setData({ dispatchtype: DISPATCH_TYPE.WORKER }); userWorkerInvocation = true; return env.JAEGER.enterSpan("dispatch_worker", async (span) => { diff --git a/packages/workers-shared/router-worker/tests/index.test.ts b/packages/workers-shared/router-worker/tests/index.test.ts index 69f0c1adffa1..3512c30574ec 100644 --- a/packages/workers-shared/router-worker/tests/index.test.ts +++ b/packages/workers-shared/router-worker/tests/index.test.ts @@ -377,4 +377,163 @@ describe("unit tests", async () => { const response = await worker.fetch(request, env, ctx); expect(response.status).toBe(304); }); + + describe("free tier limiting", () => { + it("returns fetch from asset worker for assets", async () => { + const request = new Request("https://example.com/asset"); + const ctx = createExecutionContext(); + + const env = { + CONFIG: { + has_user_worker: true, + }, + EYEBALL_CONFIG: { limitedAssetsOnly: true }, + USER_WORKER: { + async fetch(_: Request): Promise { + return new Response("hello from user worker"); + }, + }, + ASSET_WORKER: { + async fetch(_: Request): Promise { + return new Response("hello from asset worker"); + }, + async unstable_canFetch(_: Request): Promise { + return true; + }, + }, + } as Env; + + const response = await worker.fetch(request, env, ctx); + expect(await response.text()).toEqual("hello from asset worker"); + }); + + it("returns error page instead of user worker when no asset found", async () => { + const request = new Request("https://example.com/asset"); + const ctx = createExecutionContext(); + + const env = { + CONFIG: { + has_user_worker: true, + }, + EYEBALL_CONFIG: { limitedAssetsOnly: true }, + USER_WORKER: { + async fetch(_: Request): Promise { + return new Response("hello from user worker"); + }, + }, + ASSET_WORKER: { + async fetch(_: Request): Promise { + return new Response("hello from asset worker"); + }, + async unstable_canFetch(_: Request): Promise { + return false; + }, + }, + } as Env; + + const response = await worker.fetch(request, env, ctx); + expect(response.status).toEqual(429); + const text = await response.text(); + expect(text).not.toEqual("hello from user worker"); + expect(text).toContain("This website has been temporarily rate limited"); + }); + + it("returns error page instead of user worker for invoke_user_worker_ahead_of_assets", async () => { + const request = new Request("https://example.com/asset"); + const ctx = createExecutionContext(); + + const env = { + CONFIG: { + has_user_worker: true, + invoke_user_worker_ahead_of_assets: true, + }, + EYEBALL_CONFIG: { limitedAssetsOnly: true }, + USER_WORKER: { + async fetch(_: Request): Promise { + return new Response("hello from user worker"); + }, + }, + ASSET_WORKER: { + async fetch(_: Request): Promise { + return new Response("hello from asset worker"); + }, + async unstable_canFetch(_: Request): Promise { + return true; + }, + }, + } as Env; + + const response = await worker.fetch(request, env, ctx); + expect(response.status).toEqual(429); + const text = await response.text(); + expect(text).not.toEqual("hello from user worker"); + expect(text).toContain("This website has been temporarily rate limited"); + }); + + it("returns error page instead of user worker for user_worker rules", async () => { + const request = new Request("https://example.com/api/asset"); + const ctx = createExecutionContext(); + + const env = { + CONFIG: { + has_user_worker: true, + static_routing: { + user_worker: ["/api/*"], + }, + }, + EYEBALL_CONFIG: { limitedAssetsOnly: true }, + USER_WORKER: { + async fetch(_: Request): Promise { + return new Response("hello from user worker"); + }, + }, + ASSET_WORKER: { + async fetch(_: Request): Promise { + return new Response("hello from asset worker"); + }, + async unstable_canFetch(_: Request): Promise { + return true; + }, + }, + } as Env; + + const response = await worker.fetch(request, env, ctx); + expect(response.status).toEqual(429); + const text = await response.text(); + expect(text).not.toEqual("hello from user worker"); + expect(text).toContain("This website has been temporarily rate limited"); + }); + + it("returns fetch from asset worker for asset_worker rules", async () => { + const request = new Request("https://example.com/api/asset"); + const ctx = createExecutionContext(); + + const env = { + CONFIG: { + has_user_worker: true, + static_routing: { + user_worker: ["/api/*"], + asset_worker: ["/api/asset"], + }, + }, + EYEBALL_CONFIG: { limitedAssetsOnly: true }, + USER_WORKER: { + async fetch(_: Request): Promise { + return new Response("hello from user worker"); + }, + }, + ASSET_WORKER: { + async fetch(_: Request): Promise { + return new Response("hello from asset worker"); + }, + async unstable_canFetch(_: Request): Promise { + return true; + }, + }, + } as Env; + + const response = await worker.fetch(request, env, ctx); + expect(await response.text()).toEqual("hello from asset worker"); + }); + }); }); diff --git a/packages/workers-shared/router-worker/types.d.ts b/packages/workers-shared/router-worker/types.d.ts new file mode 100644 index 000000000000..6d78d737f920 --- /dev/null +++ b/packages/workers-shared/router-worker/types.d.ts @@ -0,0 +1,4 @@ +declare module "*.html" { + const content: string; + export default content; +} From 88ea269855b925e79f0417bab417934cb30068a7 Mon Sep 17 00:00:00 2001 From: Matthew Rodgers Date: Fri, 23 May 2025 15:47:47 -0700 Subject: [PATCH 4/5] WC-3584 Add changeset --- .changeset/tidy-bobcats-lose.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tidy-bobcats-lose.md diff --git a/.changeset/tidy-bobcats-lose.md b/.changeset/tidy-bobcats-lose.md new file mode 100644 index 000000000000..06ba5c4e919e --- /dev/null +++ b/.changeset/tidy-bobcats-lose.md @@ -0,0 +1,5 @@ +--- +"@cloudflare/workers-shared": minor +--- + +Limit free tier requests in the Router worker From dbe60264166a3f65c4722b04f9fe8e621ff6ef5e Mon Sep 17 00:00:00 2001 From: Matthew Rodgers Date: Thu, 12 Jun 2025 17:18:56 -0700 Subject: [PATCH 5/5] WC-3584 Move the limited response page to be inlined There's no loader configured for html files by default in wrangler it seems. So rather than mess with loaders or a custom build, inlining this page is easy enough. This pages copies what fl returns exactly, and there's a few templated things to reproduce that. Namely: the hostname in the title of the page, the ip address of the eyeball, and the current date (with some formatting) --- .../router-worker/src/limited-response.html | 21 - .../router-worker/src/limited-response.ts | 1480 +++++++++++++++++ .../router-worker/src/worker.ts | 4 +- .../workers-shared/router-worker/types.d.ts | 4 - 4 files changed, 1482 insertions(+), 27 deletions(-) delete mode 100644 packages/workers-shared/router-worker/src/limited-response.html create mode 100644 packages/workers-shared/router-worker/src/limited-response.ts delete mode 100644 packages/workers-shared/router-worker/types.d.ts diff --git a/packages/workers-shared/router-worker/src/limited-response.html b/packages/workers-shared/router-worker/src/limited-response.html deleted file mode 100644 index 0e851e10c98b..000000000000 --- a/packages/workers-shared/router-worker/src/limited-response.html +++ /dev/null @@ -1,21 +0,0 @@ - - - This website is temporarily limited - - -

This website has been temporarily rate limited

-

- You cannot access this site because the owner has reached their plan - limits. Check back later once traffic has gone down. -

-

- If you are owner of this website, prevent this from happening again by - upgrading your plan on the - Cloudflare Workers dashboard. -

- - diff --git a/packages/workers-shared/router-worker/src/limited-response.ts b/packages/workers-shared/router-worker/src/limited-response.ts new file mode 100644 index 000000000000..608429d4b20a --- /dev/null +++ b/packages/workers-shared/router-worker/src/limited-response.ts @@ -0,0 +1,1480 @@ +function formatDate(date: Date) { + const formatter = new Intl.DateTimeFormat("en-CA", { + // en-CA for YYYY-MM-DD + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hourCycle: "h23", // Ensures 24-hour format + timeZone: "UTC", + }); + const parts = formatter.formatToParts(date); + let year, month, day, hour, minute, second; + for (const part of parts) { + switch (part.type) { + case "year": + year = part.value; + break; + case "month": + month = part.value; + break; + case "day": + day = part.value; + break; + case "hour": + hour = part.value; + break; + case "minute": + minute = part.value; + break; + case "second": + second = part.value; + break; + } + } + return `${year}-${month}-${day} ${hour}:${minute}:${second} UTC`; +} + +export function renderLimitedResponse(req: Request) { + const hostname = new URL(req.url).hostname; + const ip = req.headers.get("cf-connecting-ip") ?? ""; + const ray = req.headers.get("cf-ray") ?? ""; + const date = formatDate(new Date()); + + return ` + + + + + + + + + + This website has been temporarily rate limited | + ${hostname} | Cloudflare + + + + + + + + + + + + + + + + + +
+ +
+
+

+ Please check back later +

+

Error 1027

+
+ +
+

+ + + + + + + This website has been temporarily rate limited + +

+

+ You cannot access this site because the owner has + reached their plan limits. Check back later once traffic + has gone down. +

+ +

+ If you are owner of this website, prevent this from + happening again by upgrading your plan on the + Cloudflare Workers dashboard. +

+ +

+ + Learn more about this issue → + +

+
+ + + +
+
+ + + +`; +} diff --git a/packages/workers-shared/router-worker/src/worker.ts b/packages/workers-shared/router-worker/src/worker.ts index 6a672f524a5d..551c1c15a5d7 100644 --- a/packages/workers-shared/router-worker/src/worker.ts +++ b/packages/workers-shared/router-worker/src/worker.ts @@ -7,7 +7,7 @@ import { applyEyeballConfigDefaults, applyRouterConfigDefaults, } from "./configuration"; -import limitedResponse from "./limited-response.html"; +import { renderLimitedResponse } from "./limited-response"; import type AssetWorker from "../../asset-worker"; import type { EyeballRouterConfig, @@ -96,7 +96,7 @@ export default { } if (eyeballConfig.limitedAssetsOnly) { analytics.setData({ userWorkerFreeTierLimiting: true }); - return new Response(limitedResponse, { + return new Response(renderLimitedResponse(maybeSecondRequest), { status: 429, headers: { "Content-Type": "text/html", diff --git a/packages/workers-shared/router-worker/types.d.ts b/packages/workers-shared/router-worker/types.d.ts deleted file mode 100644 index 6d78d737f920..000000000000 --- a/packages/workers-shared/router-worker/types.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module "*.html" { - const content: string; - export default content; -}