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

Skip to content

Commit 6805630

Browse files
committed
wrangler: move ratelimit binding to stable
1 parent 4cb3370 commit 6805630

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,
@@ -4064,6 +4065,121 @@ describe("normalizeAndValidateConfig()", () => {
40644065
});
40654066
});
40664067

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

317324
const fetchers: Record<string, ServiceFetch> = {};
@@ -401,6 +408,9 @@ export async function convertBindingsToCfWorkerInitBindings(
401408
} else if (binding.type === "unsafe_hello_world") {
402409
bindings.unsafe_hello_world ??= [];
403410
bindings.unsafe_hello_world.push({ ...binding, binding: name });
411+
} else if (binding.type === "ratelimit") {
412+
bindings.ratelimits ??= [];
413+
bindings.ratelimits.push({ ...binding, name: name });
404414
} else if (isUnsafeBindingType(binding.type)) {
405415
bindings.unsafe ??= {
406416
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
@@ -1119,6 +1119,29 @@ export interface EnvironmentNonInheritable {
11191119
/** Whether the timer is enabled */
11201120
enable_timer?: boolean;
11211121
}[];
1122+
1123+
/**
1124+
* Specifies rate limit bindings that are bound to this Worker environment.
1125+
*
1126+
* NOTE: This field is not automatically inherited from the top level environment,
1127+
* and so must be specified in every named environment.
1128+
*
1129+
* @default []
1130+
* @nonInheritable
1131+
*/
1132+
ratelimits: {
1133+
/** The binding name used to refer to the rate limiter in the Worker. */
1134+
name: string;
1135+
/** The namespace ID for this rate limiter. */
1136+
namespace_id: string;
1137+
/** Simple rate limiting configuration. */
1138+
simple: {
1139+
/** The maximum number of requests allowed in the time period. */
1140+
limit: number;
1141+
/** The time period in seconds (10 for ten seconds, 60 for one minute). */
1142+
period: 10 | 60;
1143+
};
1144+
}[];
11221145
}
11231146

11241147
/**

0 commit comments

Comments
 (0)