refactor: remove all rate limiting across the application (Task 11)

This commit is contained in:
2026-05-28 00:24:33 +08:00
parent ea4907cf4b
commit 893f6356c6
8 changed files with 67 additions and 286 deletions
-66
View File
@@ -1,66 +0,0 @@
import { describe, it, expect } from "vitest"
import { rateLimit } from "../rate-limit"
import { Elysia } from "elysia"
describe("rateLimit plugin", () => {
it("creates an Elysia plugin instance", () => {
const plugin = rateLimit("default")
expect(plugin).toBeInstanceOf(Elysia)
})
it("sets rate limit headers on allowed requests", async () => {
const app = new Elysia()
.use(rateLimit("default"))
.get("/test", () => "ok")
const response = await app.handle(
new Request("http://localhost/test")
)
expect(response.status).toBe(200)
expect(response.headers.get("X-RateLimit-Limit")).toBeTruthy()
expect(response.headers.get("X-RateLimit-Remaining")).toBeTruthy()
expect(response.headers.get("X-RateLimit-Reset")).toBeTruthy()
})
it("allows requests within limit", async () => {
const app = new Elysia()
.use(rateLimit("default"))
.get("/test", () => "ok")
// Make a reasonable number of requests that should all succeed
for (let i = 0; i < 5; i++) {
const response = await app.handle(
new Request("http://localhost/test")
)
expect(response.status).toBe(200)
}
})
it("different categories have independent limits", async () => {
const app = new Elysia()
.use(rateLimit("default"))
.get("/test", () => "ok")
// Just verify it works at all — actual limit enforcement depends on the config
const response = await app.handle(new Request("http://localhost/test"))
expect(response.status).toBe(200)
})
it("returns Retry-After header on rate limit", async () => {
const app = new Elysia()
.use(rateLimit("strict"))
.get("/test", () => "ok")
// Make requests until the remaining goes to 0, then check Retry-After
for (let i = 0; i < 10; i++) {
const response = await app.handle(new Request("http://localhost/test"))
if (response.status === 429) {
const retryAfter = response.headers.get("Retry-After")
expect(retryAfter).toBeTruthy()
expect(Number(retryAfter)).toBeGreaterThan(0)
break
}
}
})
})
-88
View File
@@ -1,88 +0,0 @@
import { Elysia } from "elysia"
type RateLimitEntry = {
count: number
resetAt: number
}
// NOTE: This is an in-memory rate limiter for development/single-instance
// deployments. For production with multiple instances or serverless, use a
// shared store like Redis or Upstash.
const store = new Map<string, RateLimitEntry>()
// Clean up expired entries every 60 seconds
setInterval(() => {
const now = Date.now()
for (const [key, entry] of store) {
if (now > entry.resetAt) {
store.delete(key)
}
}
}, 60_000)
function getClientIP(request: Request): string {
const forwarded = request.headers.get("x-forwarded-for")
if (forwarded) {
return forwarded.split(",")[0].trim()
}
return "unknown"
}
function checkRateLimit(
key: string,
window: number,
max: number,
): { allowed: boolean; remaining: number; resetAt: number } {
const now = Date.now()
const entry = store.get(key)
if (!entry || now > entry.resetAt) {
const resetAt = now + window * 1000
store.set(key, { count: 1, resetAt })
return { allowed: true, remaining: max - 1, resetAt }
}
if (entry.count >= max) {
return { allowed: false, remaining: 0, resetAt: entry.resetAt }
}
entry.count++
return { allowed: true, remaining: max - entry.count, resetAt: entry.resetAt }
}
const CATEGORY_LIMITS: Record<string, { window: number; max: number }> = {
default: { window: 60, max: 100 },
auth: { window: 60, max: 20 },
read: { window: 60, max: 300 },
write: { window: 60, max: 10 },
strict: { window: 60, max: 5 },
}
export const rateLimit = (
category: string = "default",
) => {
const { window, max } = CATEGORY_LIMITS[category] ?? CATEGORY_LIMITS.default
return new Elysia({ name: `rate-limit-${category}` }).onRequest(({ request, set }) => {
const ip = getClientIP(request)
const path = new URL(request.url).pathname
const key = `${category}:${ip}:${path}`
const result = checkRateLimit(key, window, max)
if (!result.allowed) {
const retryAfter = Math.ceil((result.resetAt - Date.now()) / 1000)
set.status = 429
set.headers["Retry-After"] = String(retryAfter)
return {
error: "Too many requests",
retryAfter,
}
}
// These headers are informational — clients can use them to throttle
set.headers["X-RateLimit-Limit"] = String(max)
set.headers["X-RateLimit-Remaining"] = String(result.remaining)
set.headers["X-RateLimit-Reset"] = String(Math.ceil(result.resetAt / 1000))
})
}