Skip to content

Commit 29992c3

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 3306228 commit 29992c3

File tree

4 files changed

+196
-0
lines changed

4 files changed

+196
-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
@@ -34,6 +34,8 @@ type Data = {
3434
userWorkerAhead?: boolean;
3535
// double6 - Routing performed based on the _routes.json (if provided)
3636
staticRoutingDecision?: STATIC_ROUTING_DECISION;
37+
// double7 - User worker invocation denied due to free tier limiting
38+
userWorkerFreeTierLimiting?: boolean;
3739

3840
// -- Blobs --
3941
// blob1 - Hostname of the request
@@ -89,6 +91,7 @@ export class Analytics {
8991
? -1
9092
: Number(this.data.userWorkerAhead),
9193
this.data.staticRoutingDecision ?? STATIC_ROUTING_DECISION.NOT_PROVIDED, // double6
94+
this.data.userWorkerFreeTierLimiting ? 1 : 0, // double7
9295
],
9396
blobs: [
9497
this.data.hostname?.substring(0, 256), // blob1 - trim to 256 bytes

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { setupSentry } from "../../utils/sentry";
44
import { mockJaegerBinding } from "../../utils/tracing";
55
import { Analytics, DISPATCH_TYPE, STATIC_ROUTING_DECISION } from "./analytics";
66
import { applyConfigurationDefaults } from "./configuration";
7+
/* @ts-ignore */
8+
import limitedResponse from "./limited-response.html";
79
import type AssetWorker from "../../asset-worker/src/index";
810
import type {
911
EyeballRouterConfig,
@@ -90,6 +92,15 @@ export default {
9092
"Fetch for user worker without having a user worker binding"
9193
);
9294
}
95+
if (eyeballConfig.limitedAssetsOnly) {
96+
analytics.setData({ userWorkerFreeTierLimiting: true });
97+
return new Response(limitedResponse, {
98+
status: 429,
99+
headers: {
100+
"Content-Type": "text/html",
101+
},
102+
});
103+
}
93104
analytics.setData({ dispatchtype: DISPATCH_TYPE.WORKER });
94105
userWorkerInvocation = true;
95106
return env.JAEGER.enterSpan("dispatch_worker", async (span) => {
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/tests/index.test.ts

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,4 +251,165 @@ describe("unit tests", async () => {
251251
const response = await worker.fetch(request, env, ctx);
252252
expect(await response.text()).toEqual("hello from user worker");
253253
});
254+
255+
describe("free tier limiting", () => {
256+
it("returns fetch from asset worker for assets", async () => {
257+
const request = new Request("https://example.com/asset");
258+
const ctx = createExecutionContext();
259+
260+
const env = {
261+
CONFIG: {
262+
has_user_worker: true,
263+
},
264+
EYEBALL_CONFIG: { limitedAssetsOnly: true },
265+
USER_WORKER: {
266+
async fetch(_: Request): Promise<Response> {
267+
return new Response("hello from user worker");
268+
},
269+
},
270+
ASSET_WORKER: {
271+
async fetch(_: Request): Promise<Response> {
272+
return new Response("hello from asset worker");
273+
},
274+
async unstable_canFetch(_: Request): Promise<boolean> {
275+
return true;
276+
},
277+
},
278+
} as Env;
279+
280+
const response = await worker.fetch(request, env, ctx);
281+
expect(await response.text()).toEqual("hello from asset worker");
282+
});
283+
284+
it("returns error page instead of user worker when no asset found", async () => {
285+
const request = new Request("https://example.com/asset");
286+
const ctx = createExecutionContext();
287+
288+
const env = {
289+
CONFIG: {
290+
has_user_worker: true,
291+
},
292+
EYEBALL_CONFIG: { limitedAssetsOnly: true },
293+
USER_WORKER: {
294+
async fetch(_: Request): Promise<Response> {
295+
return new Response("hello from user worker");
296+
},
297+
},
298+
ASSET_WORKER: {
299+
async fetch(_: Request): Promise<Response> {
300+
return new Response("hello from asset worker");
301+
},
302+
async unstable_canFetch(_: Request): Promise<boolean> {
303+
return false;
304+
},
305+
},
306+
} as Env;
307+
308+
const response = await worker.fetch(request, env, ctx);
309+
expect(response.status).toEqual(429);
310+
const text = await response.text();
311+
expect(text).not.toEqual("hello from user worker");
312+
expect(text).toContain("This website has been temporarily rate limited");
313+
});
314+
315+
it("returns error page instead of user worker for invoke_user_worker_ahead_of_assets", async () => {
316+
const request = new Request("https://example.com/asset");
317+
const ctx = createExecutionContext();
318+
319+
const env = {
320+
CONFIG: {
321+
has_user_worker: true,
322+
invoke_user_worker_ahead_of_assets: true,
323+
},
324+
EYEBALL_CONFIG: { limitedAssetsOnly: true },
325+
USER_WORKER: {
326+
async fetch(_: Request): Promise<Response> {
327+
return new Response("hello from user worker");
328+
},
329+
},
330+
ASSET_WORKER: {
331+
async fetch(_: Request): Promise<Response> {
332+
return new Response("hello from asset worker");
333+
},
334+
async unstable_canFetch(_: Request): Promise<boolean> {
335+
return true;
336+
},
337+
},
338+
} as Env;
339+
340+
const response = await worker.fetch(request, env, ctx);
341+
expect(response.status).toEqual(429);
342+
const text = await response.text();
343+
expect(text).not.toEqual("hello from user worker");
344+
expect(text).toContain("This website has been temporarily rate limited");
345+
});
346+
347+
it("returns error page instead of user worker for include rules", async () => {
348+
const request = new Request("https://example.com/api/asset");
349+
const ctx = createExecutionContext();
350+
351+
const env = {
352+
CONFIG: {
353+
has_user_worker: true,
354+
static_routing: {
355+
version: 1,
356+
include: ["/api/*"],
357+
},
358+
},
359+
EYEBALL_CONFIG: { limitedAssetsOnly: true },
360+
USER_WORKER: {
361+
async fetch(_: Request): Promise<Response> {
362+
return new Response("hello from user worker");
363+
},
364+
},
365+
ASSET_WORKER: {
366+
async fetch(_: Request): Promise<Response> {
367+
return new Response("hello from asset worker");
368+
},
369+
async unstable_canFetch(_: Request): Promise<boolean> {
370+
return true;
371+
},
372+
},
373+
} as Env;
374+
375+
const response = await worker.fetch(request, env, ctx);
376+
expect(response.status).toEqual(429);
377+
const text = await response.text();
378+
expect(text).not.toEqual("hello from user worker");
379+
expect(text).toContain("This website has been temporarily rate limited");
380+
});
381+
382+
it("returns fetch from asset worker for exclude rules", async () => {
383+
const request = new Request("https://example.com/api/asset");
384+
const ctx = createExecutionContext();
385+
386+
const env = {
387+
CONFIG: {
388+
has_user_worker: true,
389+
static_routing: {
390+
version: 1,
391+
include: ["/api/*"],
392+
exclude: ["/api/asset"],
393+
},
394+
},
395+
EYEBALL_CONFIG: { limitedAssetsOnly: true },
396+
USER_WORKER: {
397+
async fetch(_: Request): Promise<Response> {
398+
return new Response("hello from user worker");
399+
},
400+
},
401+
ASSET_WORKER: {
402+
async fetch(_: Request): Promise<Response> {
403+
return new Response("hello from asset worker");
404+
},
405+
async unstable_canFetch(_: Request): Promise<boolean> {
406+
return true;
407+
},
408+
},
409+
} as Env;
410+
411+
const response = await worker.fetch(request, env, ctx);
412+
expect(await response.text()).toEqual("hello from asset worker");
413+
});
414+
});
254415
});

0 commit comments

Comments
 (0)