Skip to content

Commit

Permalink
Document HTTP headers and make X-RateLimit-Resource optional
Browse files Browse the repository at this point in the history
Only add X-RateLimit-Resource if a resource name is provided
  • Loading branch information
sergiodxa committed Sep 7, 2024
1 parent 5e5cfe5 commit c5f39ab
Show file tree
Hide file tree
Showing 4 changed files with 43 additions and 18 deletions.
34 changes: 30 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,19 @@ export default {
if (!success) {
return new Response(`429 Failure – rate limit exceeded for ${pathname}`, {
status: 429,
headers: await rateLimit.writeHttpMetadata({ key: pathname }),
headers: await rateLimit.writeHttpMetadata({
key: pathname,
resource: "resource identifier", // Optional
}),
});
}

return new Response(`Success!`, {
status: 200,
headers: await rateLimit.writeHttpMetadata({ key: pathname }),
headers: await rateLimit.writeHttpMetadata({
key: pathname,
resource: "resource identifier", // Optional
}),
});
},
} satisfies ExportedHandler<Env>;
Expand All @@ -50,7 +56,10 @@ The `writeHttpMetadata` function returns an object with the necessary headers to
If you want to apply extra headers, you can either keep the result Headers object and append headers there.

```ts
let headers = await rateLimit.writeHttpMetadata({ key: pathname });
let headers = await rateLimit.writeHttpMetadata({
key: pathname,
resource: "resource identifier", // Optional
});
headers.set("X-Extra-Header", "Extra Value");
```

Expand All @@ -60,9 +69,26 @@ Or you can pass a Headers object to the `writeHttpMetadata` function and it will
let headers = new Headers();
headers.set("X-Extra-Header", "Extra Value");

await rateLimit.writeHttpMetadata({ key: pathname, headers });
await rateLimit.writeHttpMetadata(
{
key: pathname,
resource: "resource identifier", // Optional
},
headers
);
```

The `writeHttpMetadata` will set the following headers:

- `X-RateLimit-Limit`: The maximum number of requests allowed in the current window.
- `X-RateLimit-Remaining`: The number of requests remaining in the current window.
- `X-RateLimit-Used`: The number of requests used in the current window.
- `X-RateLimit-Reset`: The time in seconds when the rate limit window resets.
- `X-RateLimit-Resource`: The resource being rate limited (if provided).
- `Retry-After`: The time in seconds when the rate limit window resets.

The `X-RateLimit-Reset` and `Retry-After` headers are the same and represent the time in seconds when the rate limit window resets, the reason to duplicate them is to keep compatibility with the `Retry-After` header that is used by the HTTP standard and consistency with common `X-RateLimit-` headers.

## License

See [LICENSE](./LICENSE)
Expand Down
10 changes: 2 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
"version": "0.0.1",
"description": "A Rate Limit based on Cloudflare's Worker KV.",
"license": "MIT",
"funding": [
"https://github.com/sponsors/sergiodxa"
],
"funding": ["https://github.com/sponsors/sergiodxa"],
"author": {
"name": "Sergio Xalambrí",
"email": "hello+oss@sergiodxa.com",
Expand All @@ -31,11 +29,7 @@
"engines": {
"node": ">=20.0.0"
},
"files": [
"build",
"package.json",
"README.md"
],
"files": ["build", "package.json", "README.md"],
"exports": {
".": "./build/index.js",
"./package.json": "./package.json"
Expand Down
8 changes: 4 additions & 4 deletions src/lib/worker-kv-rate-limit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,27 +43,27 @@ describe(WorkerKVRateLimit.name, () => {
let key = "key:4";

let headers = new Headers();
await rateLimit.writeHttpMetadata({ key }, headers);
await rateLimit.writeHttpMetadata({ key, resource: "test" }, headers);

expect(headers.get("X-RateLimit-Limit")).toBe("1");
expect(headers.get("X-RateLimit-Remaining")).toBe("1");
expect(headers.get("X-RateLimit-Used")).toBe("0");
expect(new Date(Number(headers.get("X-RateLimit-Reset")))).toBeValidDate();
expect(headers.get("X-RateLimit-Resource")).toBe("key:4");
expect(headers.get("X-RateLimit-Resource")).toBe("test");
expect(new Date(Number(headers.get("Retry-After")))).toBeValidDate();
});

test("#writeHttpMetadata returns new Headers object", async () => {
let rateLimit = new WorkerKVRateLimit(kv, { limit: 1, period: 60 });
let key = "key:5";

let headers = await rateLimit.writeHttpMetadata({ key });
let headers = await rateLimit.writeHttpMetadata({ key, resource: "test" });

expect(headers.get("X-RateLimit-Limit")).toBe("1");
expect(headers.get("X-RateLimit-Remaining")).toBe("1");
expect(headers.get("X-RateLimit-Used")).toBe("0");
expect(new Date(Number(headers.get("X-RateLimit-Reset")))).toBeValidDate();
expect(headers.get("X-RateLimit-Resource")).toBe("key:5");
expect(headers.get("X-RateLimit-Resource")).toBe("test");
expect(new Date(Number(headers.get("Retry-After")))).toBeValidDate();
});
});
9 changes: 7 additions & 2 deletions src/lib/worker-kv-rate-limit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export class WorkerKVRateLimit implements RateLimit {
}

async writeHttpMetadata(
options: RateLimitOptions,
options: RateLimitOptions & { resource?: string },
headers = new Headers(),
): Promise<Headers> {
let limit = this.#limit;
Expand All @@ -115,7 +115,12 @@ export class WorkerKVRateLimit implements RateLimit {
(this.#limit - result.remaining).toString(),
);
headers.append("X-RateLimit-Reset", result.reset.toString());
headers.append("X-RateLimit-Resource", options.key);

if (options.resource) {
headers.append("X-RateLimit-Resource", options.resource);
}

// Standard HTTP header for rate limiting
headers.append("Retry-After", result.reset.toString());

return headers;
Expand Down

0 comments on commit c5f39ab

Please sign in to comment.