Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .changeset/tidy-bobcats-lose.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudflare/workers-shared": minor
---

Limit free tier requests in the Router worker
3 changes: 3 additions & 0 deletions packages/workers-shared/router-worker/src/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ type Data = {
userWorkerAhead?: boolean;
// double6 - Routing performed based on the _routes.json (if provided)
staticRoutingDecision?: STATIC_ROUTING_DECISION;
// double7 - User worker invocation denied due to free tier limiting
userWorkerFreeTierLimiting?: boolean;

// -- Blobs --
// blob1 - Hostname of the request
Expand Down Expand Up @@ -88,6 +90,7 @@ export class Analytics {
? -1
: Number(this.data.userWorkerAhead),
this.data.staticRoutingDecision ?? STATIC_ROUTING_DECISION.NOT_PROVIDED, // double6
this.data.userWorkerFreeTierLimiting ? 1 : 0, // double7
],
blobs: [
this.data.hostname?.substring(0, 256), // blob1 - trim to 256 bytes
Expand Down
14 changes: 12 additions & 2 deletions packages/workers-shared/router-worker/src/configuration.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { RouterConfig } from "../../utils/types";
import type { EyeballRouterConfig, RouterConfig } from "../../utils/types";

export const applyConfigurationDefaults = (
type RequiredEyeballRouterConfig = Required<Exclude<EyeballRouterConfig, null>>;

export const applyRouterConfigDefaults = (
configuration?: RouterConfig
): Required<RouterConfig> => {
return {
Expand All @@ -15,3 +17,11 @@ export const applyConfigurationDefaults = (
},
};
};

export const applyEyeballConfigDefaults = (
eyeballConfiguration?: EyeballRouterConfig
): RequiredEyeballRouterConfig => {
return {
limitedAssetsOnly: eyeballConfiguration?.limitedAssetsOnly ?? false,
};
};
145 changes: 83 additions & 62 deletions packages/workers-shared/router-worker/src/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,46 @@ 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,
} from "../../utils/types";
import type { ColoMetadata, Environment, ReadyAnalytics } from "./types";

const limitedResponse = `<html>
<head>
<title>This website is temporarily limited</title>
</head>
<body>
<h1>This website has been temporarily rate limited</h1>
<p>
You cannot access this site because the owner has reached their plan
limits. Check back later once traffic has gone down.
</p>
<p>
If you are owner of this website, prevent this from happening again by
upgrading your plan on the
<a
href="https://dash.cloudflare.com/?account=workers/plans"
target="_blank"
>Cloudflare Workers dashboard</a
>.
</p>
</body>
</html>`;

export interface Env {
ASSET_WORKER: Service<AssetWorker>;
USER_WORKER: Fetcher;
CONFIG: RouterConfig;
EYEBALL_CONFIG: EyeballRouterConfig;

SENTRY_DSN: string;
ENVIRONMENT: Environment;
Expand Down Expand Up @@ -55,7 +82,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);

Expand All @@ -77,6 +105,54 @@ export default {

const maybeSecondRequest = request.clone();

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"
);
}
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) => {
span.setTags({
hasUserWorker: true,
asset: asset,
dispatchType: DISPATCH_TYPE.WORKER,
});
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
const excludeRulesMatcher = generateStaticRoutingRuleMatcher(
Expand All @@ -89,18 +165,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(
Expand All @@ -118,19 +185,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;
return env.USER_WORKER.fetch(maybeSecondRequest);
});
return await routeToUserWorker({ asset: "static_routing" });
}

analytics.setData({
Expand All @@ -143,53 +200,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;
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;
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
Expand Down
Loading
Loading