Skip to content

Commit

Permalink
feat(core): added redis store and minor corrections
Browse files Browse the repository at this point in the history
  • Loading branch information
MathurAditya724 committed Mar 19, 2024
1 parent bed206a commit 19ccb5f
Show file tree
Hide file tree
Showing 17 changed files with 724 additions and 51 deletions.
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,30 @@ const limiter = rateLimiter({
windowMs: 15 * 60 * 1000, // 15 minutes
limit: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes).
standardHeaders: "draft-7", // draft-6: `RateLimit-*` headers; draft-7: combined `RateLimit` header
// store: ... , // Redis, Memcached, etc. See below.
// store: ... , // Redis, MemoryStore, etc. See below.
});

// Apply the rate limiting middleware to all requests.
app.use(limiter);
```

# Data Stores

Express-rate-limit supports external data stores to sychronize hit counts across multiple processes and servers.

By default, `MemoryStore` is used. This one does not synchronize it’s state across instances. It’s simple to deploy, and often sufficient for basic abuse prevention, but will be inconnsistent across reboots or in deployments with multiple process or servers.

Deployments requiring more consistently enforced rate limits should use an external store.

Here is a list of stores:

| Name | Description |
| ----------- | --------------------------------------------------------------------------------------------------- |
| MemoryStore | (default) Simple in-memory option. Does not share state when app has multiple processes or servers. |
| RedisStore | A [Redis](https://redis.io/)-backed store, more suitable for large or demanding deployments. |

Take a look at this [guide](https://express-rate-limit.mintlify.app/guides/creating-a-store) if you wish to create your own store.

# Contributing

We would love to have more contributors involved!
Expand Down
54 changes: 28 additions & 26 deletions apps/sandbox/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { type RateLimitInfo, rateLimiter } from "hono-rate-limiter";
import Page from "./Page";

// Init the app
const app = new Hono<{
Variables: {
rateLimit: RateLimitInfo;
};
}>();

// Adding the rate limitter
app.use(
rateLimiter({
windowMs: 10_000,
limit: 10,
handler: (_, next) => next(),
}),
);

// Routes
app.get("/", (c) => c.html(<Page info={c.get("rateLimit")} />));

// Serving the app
serve(app);
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { type RateLimitInfo, rateLimiter } from "hono-rate-limiter";
import { logger } from "hono/logger";
import Page from "./Page";

// Init the app
const app = new Hono<{
Variables: {
rateLimit: RateLimitInfo;
};
}>();

// Adding the rate limitter
app.use(
logger(),
rateLimiter({
windowMs: 10_000,
limit: 10,
handler: (_, next) => next(),
}),
);

// Routes
app.get("/", (c) => c.html(<Page info={c.get("rateLimit")} />));

// Serving the app
serve(app);
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@swc/cli": "~0.1.62",
"@swc/core": "~1.3.85",
"@swc/helpers": "~0.5.2",
"@types/ioredis-mock": "^8.2.5",
"@types/node": "18.16.9",
"@types/supertest": "^6.0.2",
"@typescript-eslint/eslint-plugin": "^6.13.2",
Expand All @@ -38,6 +39,7 @@
"eslint": "~8.48.0",
"eslint-config-prettier": "^9.0.0",
"husky": "^9.0.11",
"ioredis-mock": "^8.9.0",
"is-ci": "^3.0.1",
"lint-staged": "^15.2.2",
"ngx-deploy-npm": "^8.0.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/.swcrc
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"type": "commonjs"
},
"minify": true,
"sourceMaps": true,
"sourceMaps": false,
"exclude": [
"vite.config.ts",
".*\\.spec.tsx?$",
Expand Down
14 changes: 11 additions & 3 deletions packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
{
"name": "hono-rate-limiter",
"version": "0.0.1",
"type": "commonjs",
"dependencies": {
"@swc/helpers": "~0.5.2"
},
"peerDependencies": {
"hono": "^4.1.1"
},
"type": "commonjs",
"main": "./src/index.js",
"typings": "./src/index.d.ts"
"exports": {
".": {
"default": "./src/index.js",
"types": "./src/index.d.ts"
},
"./redis": {
"default": "./src/redis/index.js",
"types": "./src/redis/index.d.ts"
}
}
}
6 changes: 3 additions & 3 deletions packages/core/src/core/__tests__/middleware.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
} from "../../types";
import { createServer } from "./helpers";

describe.skip("middleware test", () => {
describe("middleware test", () => {
beforeEach(() => {
vi.useFakeTimers();
});
Expand Down Expand Up @@ -331,8 +331,8 @@ describe.skip("middleware test", () => {
await request(app)
.get("/")
.expect(200)
.expect("x-ratelimit-limit", "2")
.expect("x-ratelimit-remaining", "1")
.expect("ratelimit-limit", "2")
.expect("ratelimit-remaining", "1")
.expect((response) => {
if ("retry-after" in response.headers) {
throw new Error(
Expand Down
28 changes: 28 additions & 0 deletions packages/core/src/core/defaultKeyGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Context, Env, Input } from "hono";
import { getRuntimeKey } from "hono/adapter";

export function defaultKeyGenerator<
E extends Env,
P extends string,
I extends Input,
>(c: Context<E, P, I>) {
const runtime = getRuntimeKey();

let key: string | null = null;

switch (runtime) {
case "workerd":
key = c.req.raw.headers.get("CF-Connecting-IP");
break;
case "fastly":
key = c.req.raw.headers.get("Fastly-Client-IP");
break;
case "other":
key = c.req.raw.headers.get("x-real-ip");
break;
default:
break;
}

return key ?? "";
}
35 changes: 20 additions & 15 deletions packages/core/src/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { Context, Env, Input, Next } from "hono";
import { createMiddleware } from "hono/factory";
import MemoryStore from "../memcache";
import MemoryStore from "../memory-store";
import type { ConfigType, RateLimitInfo } from "../types";
import { defaultKeyGenerator } from "./defaultKeyGenerator";
import {
setDraft6Headers,
setDraft7Headers,
Expand Down Expand Up @@ -31,7 +32,7 @@ export function rateLimiter<E extends Env, P extends string, I extends Input>(
requestPropertyName = "rateLimit",
skipFailedRequests = false,
skipSuccessfulRequests = false,
keyGenerator = () => "",
keyGenerator = defaultKeyGenerator,
skip = () => false,
requestWasSuccessful = (c: Context<E, P, I>) => c.res.status < 400,
handler = async (
Expand Down Expand Up @@ -116,16 +117,6 @@ export function rateLimiter<E extends Env, P extends string, I extends Input>(
}
}

// If the client has exceeded their rate limit, set the Retry-After header
// and call the `handler` function.
if (totalHits > _limit) {
if (standardHeaders) {
setRetryAfterHeader(c, info, windowMs);
}

return handler(c, next, options);
}

// If we are to skip failed/successfull requests, decrement the
// counter accordingly once we know the status code of the request
let decremented = false;
Expand All @@ -136,9 +127,7 @@ export function rateLimiter<E extends Env, P extends string, I extends Input>(
}
};

try {
await next();

const shouldSkipRequest = async () => {
if (skipFailedRequests || skipSuccessfulRequests) {
const wasRequestSuccessful = await requestWasSuccessful(c);

Expand All @@ -148,6 +137,22 @@ export function rateLimiter<E extends Env, P extends string, I extends Input>(
)
await decrementKey();
}
};

// If the client has exceeded their rate limit, set the Retry-After header
// and call the `handler` function.
if (totalHits > _limit) {
if (standardHeaders) {
setRetryAfterHeader(c, info, windowMs);
}

await shouldSkipRequest();
return handler(c, next, options);
}

try {
await next();
await shouldSkipRequest();
} catch (error) {
if (skipFailedRequests) await decrementKey();
} finally {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type Client = {
*
* @public
*/
export default class MemoryStore implements Store {
export class MemoryStore implements Store {
/**
* The duration of time before which all hit counts are reset (in milliseconds).
*/
Expand Down Expand Up @@ -208,3 +208,5 @@ export default class MemoryStore implements Store {
this.current = new Map();
}
}

export default MemoryStore;
Loading

0 comments on commit 19ccb5f

Please sign in to comment.