mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-18 11:32:15 -05:00
fix(rate-limit): convert a string lastRequest before computing X-Retry-After (#11521)
Co-authored-by: Taesu <bytaesu@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"better-auth": patch
|
||||
---
|
||||
|
||||
Fix database-backed rate limiting returning an excessively large `X-Retry-After` header instead of the remaining seconds in the rate-limit window.
|
||||
@@ -125,7 +125,7 @@ function createDatabaseStorageWrapper(
|
||||
where: [{ field: "key", value: key }],
|
||||
});
|
||||
const data = res[0];
|
||||
if (typeof data?.lastRequest === "bigint") {
|
||||
if (data) {
|
||||
data.lastRequest = Number(data.lastRequest);
|
||||
}
|
||||
return data;
|
||||
|
||||
@@ -439,6 +439,63 @@ describe("database rate-limit cleanup scheduling", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @see https://github.com/better-auth/better-auth/issues/11519
|
||||
*/
|
||||
describe("database rate-limit with a string lastRequest", async () => {
|
||||
const { auth, client, testUser } = await getTestInstance({
|
||||
rateLimit: {
|
||||
enabled: true,
|
||||
storage: "database",
|
||||
customRules: {
|
||||
"/sign-in/email": { window: 60, max: 1 },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should respond with the remaining seconds in the retry-after header", async () => {
|
||||
const now = Date.now();
|
||||
const clock = vi.spyOn(Date, "now").mockReturnValue(now);
|
||||
// node-postgres returns `int8` columns as strings by default.
|
||||
const { adapter } = await auth.$context;
|
||||
const findMany = adapter.findMany.bind(adapter);
|
||||
vi.spyOn(adapter, "findMany").mockImplementation(async (args) => {
|
||||
const rows = await findMany(args);
|
||||
if (args.model !== "rateLimit") return rows;
|
||||
return rows.map((row) => ({
|
||||
...(row as RateLimit),
|
||||
lastRequest: String((row as RateLimit).lastRequest),
|
||||
})) as typeof rows;
|
||||
});
|
||||
|
||||
const first = await client.signIn.email({
|
||||
email: testUser.email,
|
||||
password: testUser.password,
|
||||
});
|
||||
expect(first.error).toBeNull();
|
||||
|
||||
clock.mockReturnValue(now + 1_001);
|
||||
let retryAfter = "";
|
||||
const blocked = await client.signIn.email(
|
||||
{
|
||||
email: testUser.email,
|
||||
password: testUser.password,
|
||||
},
|
||||
{
|
||||
onError(context) {
|
||||
retryAfter = context.response.headers.get("X-Retry-After") ?? "";
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(blocked.error?.status).toBe(429);
|
||||
expect(retryAfter).toBe("59");
|
||||
});
|
||||
});
|
||||
|
||||
describe("custom rate limiting storage", async () => {
|
||||
const store = new Map<string, string>();
|
||||
const expirationMap = new Map<string, number>();
|
||||
|
||||
Reference in New Issue
Block a user