in packages/better-auth/src/api/rate-limiter/index.ts [112:206]
export async function onRequestRateLimit(req: Request, ctx: AuthContext) {
if (!ctx.rateLimit.enabled) {
return;
}
const path = new URL(req.url).pathname.replace(
ctx.options.basePath || "/api/auth",
"",
);
let window = ctx.rateLimit.window;
let max = ctx.rateLimit.max;
const ip = getIp(req, ctx.options);
if (!ip) {
return;
}
const key = ip + path;
const specialRules = getDefaultSpecialRules();
const specialRule = specialRules.find((rule) => rule.pathMatcher(path));
if (specialRule) {
window = specialRule.window;
max = specialRule.max;
}
for (const plugin of ctx.options.plugins || []) {
if (plugin.rateLimit) {
const matchedRule = plugin.rateLimit.find((rule) =>
rule.pathMatcher(path),
);
if (matchedRule) {
window = matchedRule.window;
max = matchedRule.max;
break;
}
}
}
if (ctx.rateLimit.customRules) {
const _path = Object.keys(ctx.rateLimit.customRules).find((p) => {
if (p.includes("*")) {
const isMatch = wildcardMatch(p)(path);
return isMatch;
}
return p === path;
});
if (_path) {
const customRule = ctx.rateLimit.customRules[_path];
const resolved =
typeof customRule === "function" ? await customRule(req) : customRule;
if (resolved) {
window = resolved.window;
max = resolved.max;
}
}
}
const storage = getRateLimitStorage(ctx);
const data = await storage.get(key);
const now = Date.now();
if (!data) {
await storage.set(key, {
key,
count: 1,
lastRequest: now,
});
} else {
const timeSinceLastRequest = now - data.lastRequest;
if (shouldRateLimit(max, window, data)) {
const retryAfter = getRetryAfter(data.lastRequest, window);
return rateLimitResponse(retryAfter);
} else if (timeSinceLastRequest > window * 1000) {
// Reset the count if the window has passed since the last request
await storage.set(
key,
{
...data,
count: 1,
lastRequest: now,
},
true,
);
} else {
await storage.set(
key,
{
...data,
count: data.count + 1,
lastRequest: now,
},
true,
);
}
}
}