refactor: remove all rate limiting across the application (Task 11)
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user