Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 494d367

Browse files
qjexemily-shen
authored andcommitted
wrangler: move ratelimit binding to stable (#10541)
1 parent 521e003 commit 494d367

File tree

20 files changed

+373
-6
lines changed

20 files changed

+373
-6
lines changed

.changeset/five-drinks-stick.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
stable `ratelimit` binding
6+
7+
[Rate Limiting in Workers ](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/) is now generally available, `ratelimit` can be removed from unsafe bindings.
8+

fixtures/ratelimit-app/src/index.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ export default {
55
console.log("request log");
66

77
const { pathname } = new URL(request.url);
8+
9+
if (pathname.startsWith("/unsafe")) {
10+
const { success } = await env.UNSAFE_RATE_LIMITER.limit({
11+
key: pathname,
12+
});
13+
if (!success) {
14+
return new Response("unsafe: Slow down", { status: 429 });
15+
}
16+
return new Response("unsafe: Success");
17+
}
818
const { success } = await env.RATE_LIMITER.limit({ key: pathname });
919
if (!success) {
1020
return new Response(`Slow down`, {

fixtures/ratelimit-app/tests/index.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { unstable_startWorker } from "wrangler";
44

55
const basePath = resolve(__dirname, "..");
66

7-
describe("'wrangler dev' correctly renders pages", () => {
7+
describe("Rate limiting bindings", () => {
88
let worker: Awaited<ReturnType<typeof unstable_startWorker>>;
99

1010
beforeAll(async () => {
@@ -26,8 +26,26 @@ describe("'wrangler dev' correctly renders pages", () => {
2626
content = await response.text();
2727
expect(content).toEqual("Success");
2828

29+
response = await worker.fetch(`http://example.com`);
30+
content = await response.text();
31+
expect(content).toEqual("Success");
32+
2933
response = await worker.fetch(`http://example.com`);
3034
content = await response.text();
3135
expect(content).toEqual("Slow down");
3236
});
37+
38+
it("ratelimit unsafe binding is defined ", async ({ expect }) => {
39+
let response = await worker.fetch(`http://example.com/unsafe`);
40+
let content = await response.text();
41+
expect(content).toEqual("unsafe: Success");
42+
43+
response = await worker.fetch(`http://example.com/unsafe`);
44+
content = await response.text();
45+
expect(content).toEqual("unsafe: Success");
46+
47+
response = await worker.fetch(`http://example.com/unsafe`);
48+
content = await response.text();
49+
expect(content).toEqual("unsafe: Slow down");
50+
});
3351
});

fixtures/ratelimit-app/wrangler.jsonc

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,20 @@
22
"name": "ratelimit-app",
33
"compatibility_date": "2024-02-23",
44
"main": "src/index.js",
5+
"ratelimits": [
6+
{
7+
"name": "RATE_LIMITER",
8+
"namespace_id": "2001",
9+
"simple": {
10+
"limit": 3,
11+
"period": 60,
12+
},
13+
},
14+
],
515
"unsafe": {
616
"bindings": [
717
{
8-
"name": "RATE_LIMITER",
18+
"name": "UNSAFE_RATE_LIMITER",
919
"type": "ratelimit",
1020
"namespace_id": "1001",
1121
"simple": {

packages/wrangler/src/__tests__/config/configuration.test.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ describe("normalizeAndValidateConfig()", () => {
105105
r2_buckets: [],
106106
secrets_store_secrets: [],
107107
unsafe_hello_world: [],
108+
ratelimits: [],
108109
services: [],
109110
analytics_engine_datasets: [],
110111
route: undefined,
@@ -4065,6 +4066,121 @@ describe("normalizeAndValidateConfig()", () => {
40654066
});
40664067
});
40674068

4069+
describe("[ratelimit]", () => {
4070+
it("should error if ratelimit is an object", () => {
4071+
const { diagnostics } = normalizeAndValidateConfig(
4072+
// @ts-expect-error purposely using an invalid value
4073+
{ ratelimits: {} },
4074+
undefined,
4075+
undefined,
4076+
{ env: undefined }
4077+
);
4078+
4079+
expect(diagnostics.hasWarnings()).toBe(false);
4080+
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
4081+
"Processing wrangler configuration:
4082+
- The field \\"ratelimits\\" should be an array but got {}."
4083+
`);
4084+
});
4085+
4086+
it("should error if ratelimit is null", () => {
4087+
const { diagnostics } = normalizeAndValidateConfig(
4088+
// @ts-expect-error purposely using an invalid value
4089+
{ ratelimits: null },
4090+
undefined,
4091+
undefined,
4092+
{ env: undefined }
4093+
);
4094+
4095+
expect(diagnostics.hasWarnings()).toBe(false);
4096+
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
4097+
"Processing wrangler configuration:
4098+
- The field \\"ratelimits\\" should be an array but got null."
4099+
`);
4100+
});
4101+
4102+
it("should accept valid bindings", () => {
4103+
const { diagnostics } = normalizeAndValidateConfig(
4104+
{
4105+
ratelimits: [
4106+
{
4107+
name: "RATE_LIMITER",
4108+
namespace_id: "1001",
4109+
simple: {
4110+
limit: 5,
4111+
period: 60,
4112+
},
4113+
},
4114+
],
4115+
},
4116+
undefined,
4117+
undefined,
4118+
{ env: undefined }
4119+
);
4120+
4121+
expect(diagnostics.hasErrors()).toBe(false);
4122+
});
4123+
4124+
it("should error if ratelimit bindings are not valid", () => {
4125+
const { diagnostics } = normalizeAndValidateConfig(
4126+
{
4127+
ratelimits: [
4128+
// @ts-expect-error Test if empty object is caught
4129+
{},
4130+
// @ts-expect-error Test if simple is missing
4131+
{
4132+
name: "VALID",
4133+
namespace_id: "1001",
4134+
},
4135+
{
4136+
// @ts-expect-error Test if name is not a string
4137+
name: null,
4138+
// @ts-expect-error Test if namespace_id is not a string
4139+
namespace_id: 123,
4140+
simple: {
4141+
// @ts-expect-error Test if limit is not a number
4142+
limit: "not a number",
4143+
// @ts-expect-error Test if period is invalid
4144+
period: 90,
4145+
},
4146+
invalid: true,
4147+
},
4148+
{
4149+
name: "MISSING_PERIOD",
4150+
namespace_id: "1002",
4151+
// @ts-expect-error Test if period is missing
4152+
simple: {
4153+
limit: 10,
4154+
},
4155+
},
4156+
],
4157+
},
4158+
undefined,
4159+
undefined,
4160+
{ env: undefined }
4161+
);
4162+
4163+
expect(diagnostics.hasWarnings()).toBe(true);
4164+
expect(diagnostics.renderWarnings()).toMatchInlineSnapshot(`
4165+
"Processing wrangler configuration:
4166+
- Unexpected fields found in ratelimits[2] field: \\"invalid\\""
4167+
`);
4168+
expect(diagnostics.hasErrors()).toBe(true);
4169+
expect(diagnostics.renderErrors()).toMatchInlineSnapshot(`
4170+
"Processing wrangler configuration:
4171+
- \\"ratelimits[0]\\" bindings must have a string \\"name\\" field but got {}.
4172+
- \\"ratelimits[0]\\" bindings must have a string \\"namespace_id\\" field but got {}.
4173+
- \\"ratelimits[0]\\" bindings must have a \\"simple\\" configuration object but got {}.
4174+
- \\"ratelimits[1]\\" bindings must have a \\"simple\\" configuration object but got {\\"name\\":\\"VALID\\",\\"namespace_id\\":\\"1001\\"}.
4175+
- \\"ratelimits[2]\\" bindings must have a string \\"name\\" field but got {\\"name\\":null,\\"namespace_id\\":123,\\"simple\\":{\\"limit\\":\\"not a number\\",\\"period\\":90},\\"invalid\\":true}.
4176+
- \\"ratelimits[2]\\" bindings must have a string \\"namespace_id\\" field but got {\\"name\\":null,\\"namespace_id\\":123,\\"simple\\":{\\"limit\\":\\"not a number\\",\\"period\\":90},\\"invalid\\":true}.
4177+
- \\"ratelimits[2]\\" bindings \\"simple.limit\\" must be a number but got {\\"limit\\":\\"not a number\\",\\"period\\":90}.
4178+
- \\"ratelimits[2]\\" bindings \\"simple.period\\" must be either 10 or 60 but got 90.
4179+
- \\"ratelimits[3]\\" bindings \\"simple.period\\" is required and must be a number but got {\\"limit\\":10}."
4180+
`);
4181+
});
4182+
});
4183+
40684184
describe("[unsafe.bindings]", () => {
40694185
it("should error if unsafe is an array", () => {
40704186
const { diagnostics } = normalizeAndValidateConfig(

packages/wrangler/src/__tests__/type-generation.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,16 @@ const bindingsConfigMock: Omit<
237237
binding: "ASSETS_BINDING",
238238
directory: "/assets",
239239
},
240+
ratelimits: [
241+
{
242+
name: "RATE_LIMITER",
243+
namespace_id: "1001",
244+
simple: {
245+
limit: 5,
246+
period: 60,
247+
},
248+
},
249+
],
240250
};
241251

242252
describe("generate types", () => {
@@ -449,6 +459,7 @@ describe("generate types", () => {
449459
D1_TESTING_SOMETHING: D1Database;
450460
SECRET: SecretsStoreSecret;
451461
HELLO_WORLD: HelloWorldBinding;
462+
RATE_LIMITER: RateLimit;
452463
SERVICE_BINDING: Fetcher /* service_name */;
453464
OTHER_SERVICE_BINDING: Service /* entrypoint FakeEntrypoint from service_name_2 */;
454465
OTHER_SERVICE_BINDING_ENTRYPOINT: Service /* entrypoint RealEntrypoint from service_name_2 */;
@@ -543,6 +554,7 @@ describe("generate types", () => {
543554
D1_TESTING_SOMETHING: D1Database;
544555
SECRET: SecretsStoreSecret;
545556
HELLO_WORLD: HelloWorldBinding;
557+
RATE_LIMITER: RateLimit;
546558
SERVICE_BINDING: Fetcher /* service_name */;
547559
OTHER_SERVICE_BINDING: Service /* entrypoint FakeEntrypoint from service_name_2 */;
548560
OTHER_SERVICE_BINDING_ENTRYPOINT: Service /* entrypoint RealEntrypoint from service_name_2 */;
@@ -701,6 +713,7 @@ describe("generate types", () => {
701713
D1_TESTING_SOMETHING: D1Database;
702714
SECRET: SecretsStoreSecret;
703715
HELLO_WORLD: HelloWorldBinding;
716+
RATE_LIMITER: RateLimit;
704717
SERVICE_BINDING: Service<typeof import(\\"../b/index\\").default>;
705718
OTHER_SERVICE_BINDING: Service /* entrypoint FakeEntrypoint from service_name_2 */;
706719
OTHER_SERVICE_BINDING_ENTRYPOINT: Service<typeof import(\\"../c/index\\").RealEntrypoint>;

packages/wrangler/src/api/startDevWorker/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type {
2424
CfPipeline,
2525
CfQueue,
2626
CfR2Bucket,
27+
CfRateLimit,
2728
CfScriptFormat,
2829
CfSecretsStoreSecrets,
2930
CfSendEmailBindings,
@@ -302,6 +303,7 @@ export type Binding =
302303
| ({ type: "secrets_store_secret" } & BindingOmit<CfSecretsStoreSecrets>)
303304
| ({ type: "logfwdr" } & NameOmit<CfLogfwdrBinding>)
304305
| ({ type: "unsafe_hello_world" } & BindingOmit<CfHelloWorld>)
306+
| ({ type: "ratelimit" } & NameOmit<CfRateLimit>)
305307
| { type: `unsafe_${string}` }
306308
| { type: "assets" };
307309

packages/wrangler/src/api/startDevWorker/utils.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,12 @@ export function convertCfWorkerInitBindingsToBindings(
281281
}
282282
break;
283283
}
284+
case "ratelimits": {
285+
for (const { name, ...x } of info) {
286+
output[name] = { type: "ratelimit", ...x };
287+
}
288+
break;
289+
}
284290
default: {
285291
assertNever(type);
286292
}
@@ -324,6 +330,7 @@ export async function convertBindingsToCfWorkerInitBindings(
324330
assets: undefined,
325331
pipelines: undefined,
326332
unsafe_hello_world: undefined,
333+
ratelimits: undefined,
327334
};
328335

329336
const fetchers: Record<string, ServiceFetch> = {};
@@ -413,6 +420,9 @@ export async function convertBindingsToCfWorkerInitBindings(
413420
} else if (binding.type === "unsafe_hello_world") {
414421
bindings.unsafe_hello_world ??= [];
415422
bindings.unsafe_hello_world.push({ ...binding, binding: name });
423+
} else if (binding.type === "ratelimit") {
424+
bindings.ratelimits ??= [];
425+
bindings.ratelimits.push({ ...binding, name: name });
416426
} else if (isUnsafeBindingType(binding.type)) {
417427
bindings.unsafe ??= {
418428
bindings: [],

packages/wrangler/src/config/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,7 @@ export const defaultWranglerConfig: Config = {
326326
images: undefined,
327327
version_metadata: undefined,
328328
unsafe_hello_world: [],
329+
ratelimits: [],
329330

330331
/*====================================================*/
331332
/* Fields supported by Workers only */

packages/wrangler/src/config/environment.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,6 +1148,29 @@ export interface EnvironmentNonInheritable {
11481148
/** Whether the timer is enabled */
11491149
enable_timer?: boolean;
11501150
}[];
1151+
1152+
/**
1153+
* Specifies rate limit bindings that are bound to this Worker environment.
1154+
*
1155+
* NOTE: This field is not automatically inherited from the top level environment,
1156+
* and so must be specified in every named environment.
1157+
*
1158+
* @default []
1159+
* @nonInheritable
1160+
*/
1161+
ratelimits: {
1162+
/** The binding name used to refer to the rate limiter in the Worker. */
1163+
name: string;
1164+
/** The namespace ID for this rate limiter. */
1165+
namespace_id: string;
1166+
/** Simple rate limiting configuration. */
1167+
simple: {
1168+
/** The maximum number of requests allowed in the time period. */
1169+
limit: number;
1170+
/** The time period in seconds (10 for ten seconds, 60 for one minute). */
1171+
period: 10 | 60;
1172+
};
1173+
}[];
11511174
}
11521175

11531176
/**

0 commit comments

Comments
 (0)