跳转到内容

Rate Limit ​

@vafast/rate-limit 按客户端 key 限制请求频率。每个 key 在时间窗口内最多允许 max 次请求;超限返回 429,并可写入 RateLimit-* / Retry-After 响应头。

先搞清几个概念(给新用户) ​

限流 key 是什么? ​

中间件不会「按连接」或「按路由」天然限流,而是把每次请求映射成一个字符串 key,再在 context(默认内存表)里对该 key 计数。

默认 key 来自客户端 IP 相关请求头(见下方顺序)。反代后若头不可信,或你想按用户 / API Key 限流,应自定义 generator。

skip 语义(务必记清) ​

skip 返回值行为
true跳过限流:不生成计数(若尚未生成)、不递增、不写限流头
false应用限流:生成 key(若需要)、递增、可能 429

默认 skip: () => false,表示对所有请求都限流。

skip.length 会影响何时生成 key:

  • skip.length < 2(只接收 req):先 skip(req);未跳过再 generator
  • skip.length >= 2(接收 req, key):先 generator,再 skip(req, key)

这样「仅按路径跳过」时可避免无谓的 key 计算;「按 key 决定是否跳过」时则先有 key。

计数与超限判定 ​

流程是 先 increment,再判断:

text
current >= max + 1  →  429

例如 max: 10 时,第 11 次请求会被拒绝。窗口长度由 duration(毫秒)决定;Retry-After 约为 ceil(duration / 1000) 秒。

默认内存存储与多实例 ​

DefaultContext 使用进程内 LRU 计数。多进程 / 多副本部署时,各实例计数互不相通,全局 QPS 上限大约是 max × 实例数。需要集群级限流时,实现自定义 Context(如 Redis)并传入 context 选项。

安装 ​

bash
npm install @vafast/rate-limit

快速开始 ​

typescript
import { Server, defineRoute, defineRoutes, json, serve } from 'vafast'
import { rateLimit } from '@vafast/rate-limit'

const routes = defineRoutes([
  defineRoute({
    method: 'GET',
    path: '/',
    handler: () => json({ ok: true }),
  }),
])

const server = new Server(routes)
server.use(
  rateLimit({
    max: 100,
    duration: 60_000,
  }),
)
serve({ fetch: server.fetch, port: 3000 })

用法 ​

全局限流 ​

typescript
server.use(
  rateLimit({
    max: 100,
    duration: 60_000,
  }),
)

单路由限流 ​

typescript
defineRoute({
  method: 'POST',
  path: '/login',
  middleware: [rateLimit({ max: 10, duration: 60_000 })],
  handler: () => json({ ok: true }),
})

跳过探活等路径 ​

skip 返回 true 表示跳过计数与限流:

typescript
server.use(
  rateLimit({
    max: 60,
    duration: 60_000,
    skip: (req) => {
      const path = new URL(req.url).pathname
      return path === '/' || path === '/health'
    },
  }),
)

自定义限流 key ​

typescript
rateLimit({
  max: 30,
  duration: 60_000,
  generator: (req) =>
    req.headers.get('authorization') ??
    req.headers.get('x-forwarded-for') ??
    'anonymous',
})

自定义超限响应(errorResponse 三种形态) ​

类型行为
string以 429 文本响应返回该字符串(text(...));可附带限流头
Responseclone() 后返回;若 headers: true 会把 RateLimit-* / Retry-After 写到 clone 上
Error抛出该 Error(由上层错误处理接管)
其它非上述类型回落为文本 'Too Many Requests',状态 429
typescript
import { err } from 'vafast'

rateLimit({
  max: 5,
  duration: 60_000,
  errorResponse: new Response(JSON.stringify({ error: 'Too Many Requests' }), {
    status: 429,
    headers: { 'Content-Type': 'application/json' },
  }),
  // 或抛错:errorResponse: err('Too Many Requests', 429)
  // 或纯文本:errorResponse: 'rate-limit reached'
})

自定义 Context(存储) ​

需要 Redis 等外部存储时,实现 Context 接口并传入:

方法说明
init(options)中间件创建时调用;可读取 duration / max 等(不含 context 自身)
increment(key)计数 +1,返回 { count, nextReset }
decrement(key)计数 -1;countFailedRequest: false 且下游抛错时会调用
reset(key?)重置某个 key,或不传则清空全部
kill()进程结束时的清理钩子
typescript
import type { Context } from '@vafast/rate-limit'
import { rateLimit } from '@vafast/rate-limit'

const redisContext: Context = {
  init() { /* ... */ },
  async increment(key) { /* return { count, nextReset } */ },
  async decrement(key) { /* ... */ },
  async reset(key) { /* ... */ },
  async kill() { /* ... */ },
}

rateLimit({ max: 100, duration: 60_000, context: redisContext })

API ​

导出 ​

导出说明
rateLimit(options?)主入口,返回中间件
DefaultContext默认内存计数存储(LRU,构造参数 maxSize 默认 5000)
defaultOptions默认配置常量
Options / Context / Generator相关类型

rateLimit(options?: Partial<Options>) ​

参数类型默认说明
durationnumber60000计数窗口(毫秒);也用于 Retry-After
maxnumber10窗口内最大请求数
errorResponsestring | Response | Error'rate-limit reached'超限响应,见上节三种形态
countFailedRequestbooleanfalsefalse 时下游 抛错 会 decrement 退还计数
generator(req, server, derived) => string | Promise<string>defaultKeyGenerator限流 key
contextContextnew DefaultContext()计数存储
skip(req, key?) => boolean | Promise<boolean>() => false返回 true 跳过限流
headersbooleantrue是否写 RateLimit-* / Retry-After
injectServer() => any—传给 generator 的 server;一般不需要
scoping'global' | 'scoped''global'兼容字段,当前实现未使用

默认 generator 请求头顺序 ​

defaultKeyGenerator 按以下顺序取客户端地址,命中即返回:

  1. x-real-ip
  2. x-forwarded-for(取逗号分隔的 第一个,并 trim)
  3. cf-connecting-ip
  4. x-client-ip

若全部缺失:回退为 ua:${user-agent || 'unknown'},并 console.warn。

request 为 undefined 时返回空字符串并告警。

响应头(headers: true) ​

头说明
RateLimit-Limit窗口上限(max)
RateLimit-Remaining剩余次数
RateLimit-Reset距重置的秒数(向上取整)
Retry-After仅超限时附加;约为 ceil(duration / 1000) 秒

skip 与 key 生成时机 ​

  • skip.length < 2:先调用 skip(req),未跳过再生成 key
  • skip.length >= 2:先生成 key,再调用 skip(req, key)

判断条件为 (await skip(...)) === false 时才进入计数;其它真值均视为跳过。

最佳实践 ​

  • 登录、发短信等敏感接口用更小的 max,或单独挂路由级限流。
  • 反代后务必保证 IP 相关请求头可信,或自定义 generator(例如按用户 ID / API Key)。
  • 探活、静态资源等用 skip 排除(返回 true),避免误伤监控。
  • 多实例部署时默认内存 DefaultContext 不共享;需要全局限流请实现自定义 Context。

注意事项 ​

  • scoping 仅为兼容保留,不会改变行为。
  • skip 返回 true 才跳过;默认 () => false 表示全部计数。
  • 超限判定为 current >= max + 1(先递增再判断)。
  • countFailedRequest: false 时,仅对下游 抛错 退还计数;正常 4xx/5xx 响应仍会计数。
  • 多实例 + 默认内存存储 ≠ 集群统一限流。

相关链接 ​