Skip to content

Commit 28e3889

Browse files
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.
1 parent 5305c9b commit 28e3889

File tree

5 files changed

+197
-0
lines changed

5 files changed

+197
-0
lines changed

packages/workers-shared/router-worker/src/analytics.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ type Data = {
3535
staticRoutingDecision?: STATIC_ROUTING_DECISION;
3636
// double7 - Whether the request was blocked by abuse mitigation or not
3737
abuseMitigationBlocked?: boolean;
38+
// double8 - User worker invocation denied due to free tier limiting
39+
userWorkerFreeTierLimiting?: boolean;
3840

3941
// -- Blobs --
4042
// blob1 - Hostname of the request
@@ -93,6 +95,7 @@ export class Analytics {
9395
: Number(this.data.userWorkerAhead),
9496
this.data.staticRoutingDecision ?? STATIC_ROUTING_DECISION.NOT_PROVIDED, // double6
9597
this.data.abuseMitigationBlocked ? 1 : 0, // double7
98+
this.data.userWorkerFreeTierLimiting ? 1 : 0, // double8
9699
],
97100
blobs: [
98101
this.data.hostname?.substring(0, 256), // blob1 - trim to 256 bytes
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<html>
2+
<head>
3+
<title>This website is temporarily limited</title>
4+
</head>
5+
<body>
6+
<h1>This website has been temporarily rate limited</h1>
7+
<p>
8+
You cannot access this site because the owner has reached their plan
9+
limits. Check back later once traffic has gone down.
10+
</p>
11+
<p>
12+
If you are owner of this website, prevent this from happening again by
13+
upgrading your plan on the
14+
<a
15+
href="https://dash.cloudflare.com/?account=workers/plans"
16+
target="_blank"
17+
>Cloudflare Workers dashboard</a
18+
>.
19+
</p>
20+
</body>
21+
</html>

packages/workers-shared/router-worker/src/worker.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
applyEyeballConfigDefaults,
88
applyRouterConfigDefaults,
99
} from "./configuration";
10+
import limitedResponse from "./limited-response.html";
1011
import type AssetWorker from "../../asset-worker";
1112
import type {
1213
EyeballRouterConfig,
@@ -93,6 +94,15 @@ export default {
9394
"Fetch for user worker without having a user worker binding"
9495
);
9596
}
97+
if (eyeballConfig.limitedAssetsOnly) {
98+
analytics.setData({ userWorkerFreeTierLimiting: true });
99+
return new Response(limitedResponse, {
100+
status: 429,
101+
headers: {
102+
"Content-Type": "text/html",
103+
},
104+
});
105+
}
96106
analytics.setData({ dispatchtype: DISPATCH_TYPE.WORKER });
97107
userWorkerInvocation = true;
98108
return env.JAEGER.enterSpan("dispatch_worker", async (span) => {

packages/workers-shared/router-worker/tests/index.test.ts

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,4 +377,163 @@ describe("unit tests", async () => {
377377
const response = await worker.fetch(request, env, ctx);
378378
expect(response.status).toBe(304);
379379
});
380+
381+
describe("free tier limiting", () => {
382+
it("returns fetch from asset worker for assets", async () => {
383+
const request = new Request("https://example.com/asset");
384+
const ctx = createExecutionContext();
385+
386+
const env = {
387+
CONFIG: {
388+
has_user_worker: true,
389+
},
390+
EYEBALL_CONFIG: { limitedAssetsOnly: true },
391+
USER_WORKER: {
392+
async fetch(_: Request): Promise<Response> {
393+
return new Response("hello from user worker");
394+
},
395+
},
396+
ASSET_WORKER: {
397+
async fetch(_: Request): Promise<Response> {
398+
return new Response("hello from asset worker");
399+
},
400+
async unstable_canFetch(_: Request): Promise<boolean> {
401+
return true;
402+
},
403+
},
404+
} as Env;
405+
406+
const response = await worker.fetch(request, env, ctx);
407+
expect(await response.text()).toEqual("hello from asset worker");
408+
});
409+
410+
it("returns error page instead of user worker when no asset found", async () => {
411+
const request = new Request("https://example.com/asset");
412+
const ctx = createExecutionContext();
413+
414+
const env = {
415+
CONFIG: {
416+
has_user_worker: true,
417+
},
418+
EYEBALL_CONFIG: { limitedAssetsOnly: true },
419+
USER_WORKER: {
420+
async fetch(_: Request): Promise<Response> {
421+
return new Response("hello from user worker");
422+
},
423+
},
424+
ASSET_WORKER: {
425+
async fetch(_: Request): Promise<Response> {
426+
return new Response("hello from asset worker");
427+
},
428+
async unstable_canFetch(_: Request): Promise<boolean> {
429+
return false;
430+
},
431+
},
432+
} as Env;
433+
434+
const response = await worker.fetch(request, env, ctx);
435+
expect(response.status).toEqual(429);
436+
const text = await response.text();
437+
expect(text).not.toEqual("hello from user worker");
438+
expect(text).toContain("This website has been temporarily rate limited");
439+
});
440+
441+
it("returns error page instead of user worker for invoke_user_worker_ahead_of_assets", async () => {
442+
const request = new Request("https://example.com/asset");
443+
const ctx = createExecutionContext();
444+
445+
const env = {
446+
CONFIG: {
447+
has_user_worker: true,
448+
invoke_user_worker_ahead_of_assets: true,
449+
},
450+
EYEBALL_CONFIG: { limitedAssetsOnly: true },
451+
USER_WORKER: {
452+
async fetch(_: Request): Promise<Response> {
453+
return new Response("hello from user worker");
454+
},
455+
},
456+
ASSET_WORKER: {
457+
async fetch(_: Request): Promise<Response> {
458+
return new Response("hello from asset worker");
459+
},
460+
async unstable_canFetch(_: Request): Promise<boolean> {
461+
return true;
462+
},
463+
},
464+
} as Env;
465+
466+
const response = await worker.fetch(request, env, ctx);
467+
expect(response.status).toEqual(429);
468+
const text = await response.text();
469+
expect(text).not.toEqual("hello from user worker");
470+
expect(text).toContain("This website has been temporarily rate limited");
471+
});
472+
473+
it("returns error page instead of user worker for user_worker rules", async () => {
474+
const request = new Request("https://example.com/api/asset");
475+
const ctx = createExecutionContext();
476+
477+
const env = {
478+
CONFIG: {
479+
has_user_worker: true,
480+
static_routing: {
481+
user_worker: ["/api/*"],
482+
},
483+
},
484+
EYEBALL_CONFIG: { limitedAssetsOnly: true },
485+
USER_WORKER: {
486+
async fetch(_: Request): Promise<Response> {
487+
return new Response("hello from user worker");
488+
},
489+
},
490+
ASSET_WORKER: {
491+
async fetch(_: Request): Promise<Response> {
492+
return new Response("hello from asset worker");
493+
},
494+
async unstable_canFetch(_: Request): Promise<boolean> {
495+
return true;
496+
},
497+
},
498+
} as Env;
499+
500+
const response = await worker.fetch(request, env, ctx);
501+
expect(response.status).toEqual(429);
502+
const text = await response.text();
503+
expect(text).not.toEqual("hello from user worker");
504+
expect(text).toContain("This website has been temporarily rate limited");
505+
});
506+
507+
it("returns fetch from asset worker for asset_worker rules", async () => {
508+
const request = new Request("https://example.com/api/asset");
509+
const ctx = createExecutionContext();
510+
511+
const env = {
512+
CONFIG: {
513+
has_user_worker: true,
514+
static_routing: {
515+
user_worker: ["/api/*"],
516+
asset_worker: ["/api/asset"],
517+
},
518+
},
519+
EYEBALL_CONFIG: { limitedAssetsOnly: true },
520+
USER_WORKER: {
521+
async fetch(_: Request): Promise<Response> {
522+
return new Response("hello from user worker");
523+
},
524+
},
525+
ASSET_WORKER: {
526+
async fetch(_: Request): Promise<Response> {
527+
return new Response("hello from asset worker");
528+
},
529+
async unstable_canFetch(_: Request): Promise<boolean> {
530+
return true;
531+
},
532+
},
533+
} as Env;
534+
535+
const response = await worker.fetch(request, env, ctx);
536+
expect(await response.text()).toEqual("hello from asset worker");
537+
});
538+
});
380539
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
declare module "*.html" {
2+
const content: string;
3+
export default content;
4+
}

0 commit comments

Comments
 (0)