【免费下载链接】fluxer
A free and open source instant messaging and VoIP chat app built for friends, groups, and communities.
Fluxer 是一个面向朋友、群组与社区的开源即时通讯与 VoIP 应用,其 HTTP API 通过逐路由限流桶(per-route bucket)与全局限流桶(global bucket)双层结构约束流量。本文以 fluxer_docs/src/content/docs/topics/rate-limits.md 为骨架,结合 限流中间件、限流配置聚合 与 限流服务实现 等源码,完整讲解桶的作用域规则、429 响应体、X-RateLimit-*响应头、处理器内限流(handler-level limits)与慢速模式(slowmode),帮助你在接入或运维 Fluxer API 时准确理解并优雅处理限流。
Buckets 与作用域(Buckets and scope)
Fluxer 中,一个 bucket 就是在一个时间窗口(window)内对一个允许额度(allowance)的计数。几乎所有路由都声明自己的 bucket;超出额度的请求会被拒绝,返回:
- 状态码
429; code为RATE_LIMITED;- 携带一个限流响应对象;
- 携带限流响应头。
路径参数占位符:每个资源独立额度
bucket 名称可以包含路径参数占位符,写法是冒号加参数名,例如guild:emojis:list::guild_id。Fluxer 在键控(keying)前会用请求中的实际值替换占位符,因此同一个路由对每个路径资源持有独立额度。
这一点在源码中有直接体现:中间件中的resolveBucket遍历请求路径参数,逐一替换 bucket 字符串中的:参数名占位符(见 RateLimitMiddleware.ts):
function resolveBucket(bucket: string, clientId: string, ctx: Context<HonoEnv>): string { let resolved = bucket; const params = ctx.req.param(); for (const [key, value] of Object.entries(params)) { resolved = resolved.replace(`:${key}`, String(value)); } return `${clientId}:${resolved}`; }替换后的 bucket 还会以调用者身份为前缀进行键控(见下文)。
调用者身份键控:认证、Bot、Bearer 与 IP
每个 bucket 都会按调用者身份键控:
- 已认证请求:按账号(account)与凭证类型(credential kind)键控;
- OAuth2 bearer 凭证:额外按所属应用(owning application)键控;
- 因此,会话(session)、机器人令牌(bot token)、Admin API 密钥以及每个 bearer 应用,对同一个账号会各自占用独立的额度。
源码中getClientIdentifier精确实现了这一规则(RateLimitMiddleware.ts):
if (user?.id) { const tokenType = ctx.get('authTokenType') ?? 'session'; if (tokenType === 'bearer') { return `user:${user.id}:bearer:${ctx.get('oauthBearerApplicationId') ?? 'unknown'}`; } return `user:${user.id}:${tokenType}`; }- 未解析出账号的请求:按客户端 IP 键控,IPv4 使用精确地址,IPv6 使用
/64前缀,因此同一/64网段内的客户端会共享额度。源码中getSameIpDecisionKey负责将 IP 归一化到决策键。 - 若部署配置为从某个请求头读取地址而请求没有携带该头,Fluxer 会在评估任何 bucket之前直接以
403 FORBIDDEN拒绝该请求。
全局桶:共享的全局额度
- 请求还会计入全局 bucket(除非其路由 bucket 声明为豁免);
- 全局桶按同一身份键控,因此未解析出账号的请求会消耗其客户端 IP 的全局额度;
- 中间件中,
checkGlobalLimit(clientId, globalLimit)在任何路由桶检查之前执行(RateLimitMiddleware.ts)。
豁免路由:只消费自己的额度
以下 bucket 是豁免的,且是各自路由声明的唯一bucket,它们不消耗任何全局额度:
webhook:execute::webhook_idwebhook:message_get::webhook_idwebhook:message_edit::webhook_idwebhook:message_delete::webhook_idwebhook:github::webhook_idwebhook:instatus::webhook_idstripe:webhook
源码在 WebhookRateLimitConfig.ts 与 IntegrationRateLimitConfig.ts 中确认了这些配置,例如webhook:execute::webhook_id为{limit: 60, windowMs: ms('1 minute'), exemptFromGlobal: true},stripe:webhook为{limit: 300, windowMs: ms('1 minute'), exemptFromGlobal: true}。
此外,user:group_dm:create与user:group_dm:recipient:add两个 bucket 也是豁免的,但它们是某路由上的第二个 bucket(第一个 bucket 不豁免),因此这两个路由仍然会消耗全局额度。源码中可见user:group_dm:create配置为{limit: 10, windowMs: ms('1 hour'), exemptFromGlobal: true}(UserRateLimitConfig.ts)。
全局窗口与默认额度
- 全局窗口为1 秒;
- 默认额度为每秒 50 次请求;
- 持有
HIGH_GLOBAL_RATE_LIMIT账号标记(account flags 文档见 fluxer_admin 的账号标记实现)的账号获得每秒 1200 次; - 持有
RATE_LIMIT_BYPASS标记的账号豁免于全局桶与所有路由桶,其成功响应不携带任何限流头。
上述常量在源码中都有对应实现(RateLimitMiddleware.ts):
if (user?.flags && (user.flags & UserFlags.HIGH_GLOBAL_RATE_LIMIT) !== 0n) { return 1200; } return 50;同时,RateLimitService中DEFAULT_GLOBAL_WINDOW_MS = 1000(RateLimitService.ts)也印证了 1 秒全局窗口。
两个容易混淆的注意点
:::note[An allowance drains continuously] 每个 bucket 都是漏桶(leaky bucket):一次性最多允许声明的额度,并在每个声明窗口内以该额度持续补充(refill)。因此,客户端耗尽额度后,只要桶中补充出足够额度即可再次发送。 :::
:::note[The headers and the body report different instants]X-RateLimit-Reset与X-RateLimit-Reset-After报告的是桶完全排空的时刻;而响应体中的retry_after报告的是再次准入一个请求所需等待的较短延迟。 :::
:::note[A 429 can hide a 401 or 403] 一个被限流的请求即使其凭证本应被以401或403拒绝,也可能返回429。测试 GlobalRateLimitRevokesSession.test.ts 正好验证了这一点:会话被全局限流吊销后,再次请求返回 401UNAUTHORIZED,而非 429。 :::
第二个路由桶与计数时机
部分路由声明了第二个路由桶,包括群组私信创建(group DM creation)、添加群组成员(adding group recipients)以及删除 guild 表情/贴纸(deleting guild emoji or stickers)。Fluxer 在处理器运行之前就对请求进行路由桶计数,所以被处理器拒绝的请求仍然会消耗额度。
警告:全局拒绝会吊销用户会话
:::caution[A global denial revokes a user session] 当全局桶拒绝了由非 Bot 账号的用户会话令牌认证的请求时,Fluxer 会在写回 429之前吊销该令牌,客户端必须重新认证。Bot 令牌、OAuth2 访问令牌与 Admin API 密钥绝不会被这样吊销;路由桶拒绝也绝不会吊销任何凭证。 :::
源码中的revokeAuthenticatedSessionOnGlobalRateLimit精确实现了这一行为:仅当authTokenType === 'session'且用户非 Bot 时调用AuthSession.revokeToken(RateLimitMiddleware.ts),并由测试 GlobalRateLimitRevokesSession.test.ts 验证。
通过实例配置整体关闭
部署可以通过实例配置同时禁用两类桶。禁用期间:
- 没有任何响应携带
X-RateLimit-*头; - 不会有请求以
429 RATE_LIMITED被拒绝; - 该开关同时关闭登录额度(login allowances);
- 其他所有处理器内限流仍然生效。
中间件中shouldEnforceRateLimits读取Config.dev.disableRateLimits(RateLimitMiddleware.ts),测试模式下还支持通过x-fluxer-test-enable-rate-limits请求头开启、x-fluxer-test-global-rate-limit覆盖全局额度,便于测试验证。
限流响应对象(Rate limit response object)
拒绝响应体包含普通错误响应的全部成员,并额外增加两个成员。
结构
| 字段 | 类型 | 描述 |
|---|---|---|
| code1 | string | bucket 拒绝时为RATE_LIMITED |
| message2 | string | 本地化的限流提示消息 |
| global | boolean | 拒绝是否由全局桶产生;路由桶拒绝时存在且为false |
| retry_after3 | number | 距离下一个请求被准入的延迟(小数秒) |
1在路由桶中间件之外强制执行的限流可以复用该响应体但使用自己的 code。Send phone verification 是目前唯一活跃的例子,返回PHONE_RATE_LIMIT_EXCEEDED。
2使用为请求解析出的本地化(locale),账号设置优先于 Accept-Language。
3永不低于 0.001;当没有计算出小数延迟时,回退为整秒的Retry-After值。RateLimitService中retryAfterDecimal: result.retryAfterMs > 0 ? Math.max(0.001, retryAfterDecimal) : undefined正是这一规则的实现(RateLimitService.ts)。
Resend IP authorisation 的冷却期以自己的 code 和不同的响应体回答 429,具体内容见下文回答 429 的额度。慢速模式(slowmode) 的拒绝则回答400 SLOWMODE_RATE_LIMITED。
示例
{ "code": "RATE_LIMITED", "message": "You're being rate limited.", "global": false, "retry_after": 0.428 }测试 GlobalRateLimitRevokesSession.test.ts 验证了全局拒绝时json.global === true、X-RateLimit-Global头为true、X-RateLimit-Scope为global,且X-RateLimit-Bucket头为null。
限流作用域(Rate limit scopes)
X-RateLimit-Scope头表示产生拒绝的作用域。
| 值 | 描述 |
|---|---|
| user | 拒绝来自仅按调用者键控的额度 |
| global | 拒绝来自全局桶 |
| shared1 | 拒绝来自多个账号可相互耗尽的额度 |
1没有任何路由桶声明自己的作用域,所以每个路由桶拒绝都报告user。Send phone verification 是唯一现实存在的shared来源:当每个号码的发送额度或号码级 provider 冷却产生拒绝时,两者都按提交的号码键控,因此两个账号向同一个号码发送会共享额度。
在中间件实现中,路由桶拒绝使用routeConfig.scope ?? 'user',email bucket 拒绝使用scope: 'shared'(RateLimitMiddleware.ts)。
限流响应头(Rate limit headers)
这些头描述一次限流决策。返回 429 的操作会携带这组头。
| 字段 | 类型 | 描述 |
|---|---|---|
| Retry-After?1 | string | 距下一次请求被准入的延迟(整秒,向上取整,永不小于 1) |
| X-RateLimit-Scope?1 | string | 产生拒绝的限流作用域 |
| X-RateLimit-Global?2 | string | 全局 HTTP 429 时的字面值true |
| X-RateLimit-Limit?3 | string | 路由额度一次性最多允许的请求数 |
| X-RateLimit-Remaining?3 | string | 路由额度仍允许的请求数,拒绝时恒为 0 |
| X-RateLimit-Reset?34 | string | 路由额度完全恢复可用的 Unix 秒级时间戳 |
| X-RateLimit-Reset-After?35 | string | 路由额度完全恢复可用还需的秒数 |
| X-RateLimit-Bucket?36 | string | 路由桶的稳定 16 位十六进制标识符 |
1在所有限流拒绝时发送,且绝不会出现在成功响应上。
2仅在全局桶产生拒绝时发送,此时不发送任何路由桶头。
3在路由 HTTP 429 时发送(X-RateLimit-Bucket受注 6 约束)。在成功响应上,仅当凭证解析为Bot 账号、或请求未解析出账号但位于同时具备webhook_id与token路径参数的路由时才会发送。
4值若早于或等于当前秒,会被替换为下一秒。
5成功响应上四舍五入到毫秒精度并去除尾部零;拒绝时输出精确计算的小数。
6是路由 bucket 的不透明标识符,不代表调用者。
从源码看,setRateLimitHeaders通过createHash('sha256').update(bucket).digest('hex').slice(0, 16)生成桶哈希(RateLimitMiddleware.ts),与测试中对X-RateLimit-Bucket的断言完全一致。
处理器内强制执行的限流返回 429 时携带其他路由头但没有X-RateLimit-Bucket。
:::note[Cross-origin clients cannot read rate-limit headers] 跨域策略(cross-origin policy) 不会暴露这些头。请改用响应体中的retry_after值。 :::
处理器内限流(Limits enforced inside a handler)
处理器内强制的额度独立于路由桶与全局桶键控,因此即使两类桶仍有空间,耗尽该额度也会拒绝请求。以下是完整清单。
disable_rate_limits部署开关会连同两类桶一起关闭登录额度;relax_registration_rate_limits关闭注册额度。其余所有额度在所有部署上都会强制执行。
拒绝有两种形态:
- 回答 429 的额度:携带限流响应对象与限流头(减去
X-RateLimit-Bucket); - 回答 400 的额度:回答
400 INVALID_FORM_BODY,并附带一个code指向被耗尽额度的校验错误条目。
400 形态没有retry_after成员、X-RateLimit-*头与Retry-After头,剩余延迟只出现在条目的本地化message中。
回答 429 的额度(Allowances answering 429)
| 操作 | 额度 | 错误码 |
|---|---|---|
| Log in with a password | 每 15 分钟 5 次,按提交的邮箱地址键控 | RATE_LIMITED |
| Log in with a password | 每 30 分钟 10 次,按客户端 IP 键控(IPv4 精确、IPv6 按/64) | RATE_LIMITED |
| Register an account | 每 15 分钟 3 次,按提交的邮箱键控 | RATE_LIMITED |
| Register an account | 每小时 3 次,按客户端 IP 键控 | RATE_LIMITED |
| Register an account | 每小时 15 次,按客户端子网键控(IPv4/24或 IPv6/48) | RATE_LIMITED |
| Resend email verification | 每 15 分钟 3 次,按账号存储的邮箱键控 | RATE_LIMITED |
| Request password recovery | 每 30 分钟 20 次,按客户端 IP 键控 | RATE_LIMITED |
| Request password recovery | 每 30 分钟 5 次,按提交的邮箱键控 | RATE_LIMITED |
| Start email change 与 Resend original email code | 每 15 分钟 3 次发送,按已认证账号键控 | RATE_LIMITED |
| Request new email、Resend new email code 与两种 bounced email recovery 发送 | 每 15 分钟 5 次发送,按已认证账号键控 | RATE_LIMITED |
| Start password change | 每 15 分钟 3 次发送,按已认证账号键控 | RATE_LIMITED |
| Resend password change code | 每 15 分钟 3 次发送,按已认证账号键控 | RATE_LIMITED |
| 邮箱/密码变更 ticket 上的每次 code 重发与每次新地址请求 | 每 30 秒 1 次发送,按 ticket 键控并从其上一次发送起算 | RATE_LIMITED |
| Report message、Report user、Report guild 与 Create DSA report | 每小时 5 次,按举报人键控(账号或已验证邮箱) | RATE_LIMITED |
| Report message | 每小时 3 次,按举报人与频道共同键控 | RATE_LIMITED |
| Report message | 每小时 20 次,按被举报消息键控,跨所有举报人 | RATE_LIMITED |
| Report message | 每小时 4 次,按举报人与 guild 共同键控(guild 消息) | RATE_LIMITED |
| Send phone verification | 每 6 小时 3 次,按已认证账号键控 | PHONE_RATE_LIMIT_EXCEEDED |
| Send phone verification | 每 5 天 3 次,按提交的号码键控 | PHONE_RATE_LIMIT_EXCEEDED |
| Resend IP authorisation | ticket 签发后 30 秒内无任何操作,按授权 ticket 键控 | IP_AUTHORIZATION_RESEND_COOLDOWN |
SMS provider 节流可能施加额外冷却,返回PHONE_RATE_LIMIT_EXCEEDED并携带剩余延迟。
Resend IP authorisation 冷却没有X-RateLimit-*头,只有整秒的Retry-After头,且响应体以顶层resend_available_in与retry_after再次报告该延迟。同一 ticket 上的第二次重发返回400 IP_AUTHORIZATION_RESEND_LIMIT_EXCEEDED。该额度永不补充,且 ticket 在签发 15 分钟后过期。
回答 400 的额度(Allowances answering 400)
| 操作 | 额度 | 校验错误码 |
|---|---|---|
| Modify current user | 结果用户名或判别符每 3 小时 5 次 | USERNAME_CHANGED_TOO_MANY_TIMES |
| Modify current user | 个人简介每 30 分钟 25 次(提交值不同时) | BIO_CHANGED_TOO_MANY_TIMES |
| Modify current user | 代词每 30 分钟 25 次(提交值不同时) | PRONOUNS_CHANGED_TOO_MANY_TIMES |
| Modify current user | 强调色每 30 分钟 25 次(提交值不同时) | ACCENT_COLOR_CHANGED_TOO_MANY_TIMES |
| Modify current user | 任何非 null 头像每 30 分钟 25 次 | AVATAR_CHANGED_TOO_MANY_TIMES |
| Modify current user | 通过权益检查后的任何横幅值每 30 分钟 25 次 | BANNER_CHANGED_TOO_MANY_TIMES |
| Update bot profile | Bot 结果用户名或判别符每 3 小时 5 次 | USERNAME_CHANGED_TOO_MANY_TIMES |
| Modify current guild member | guild 头像每 30 分钟 25 次(无论何时提供) | AVATAR_CHANGED_TOO_MANY_TIMES |
| Modify current guild member | guild 横幅每 30 分钟 25 次(无论何时提供) | BANNER_CHANGED_TOO_MANY_TIMES |
| Modify current guild member | guild 个人简介每 30 分钟 25 次(提交值不同时) | BIO_CHANGED_TOO_MANY_TIMES |
| Modify current guild member | guild 代词每 30 分钟 25 次(提交值不同时) | PRONOUNS_CHANGED_TOO_MANY_TIMES |
| Modify current guild member | guild 强调色每 30 分钟 25 次(提交值不同时) | ACCENT_COLOR_CHANGED_TOO_MANY_TIMES |
| Modify voice activity sharing | 分享默认值每 24 小时 1 次 | VOICE_ACTIVITY_SHARING_ON_COOLDOWN |
| Complete login with TOTP 与 Complete login with WebAuthn MFA | 每 15 分钟 10 次多因素尝试 | INVALID_CODE |
| Complete login with TOTP 与 Complete login with WebAuthn MFA | 单个 MFA ticket 上每 5 分钟 5 次多因素尝试 | INVALID_CODE |
Sudo mode(totp方法) | 每 15 分钟 10 次多因素尝试 | INVALID_MFA_CODE |
键控归属:每个 Modify current user 额度按已认证账号键控,Bot 标签额度按 Bot 账号键控——因此所有者修改 Bot 标签会消耗 Bot 的额度。guild 成员额度按guild 与成员共同键控,一个账号在每个 guild 中持有独立额度。登录额度分别按账号与 MFA ticket 键控,sudo 额度按账号键控。
关键行为:Fluxer 在检查 code 之前就消耗每个多因素额度,因此用正确 code 撞上已耗尽额度时,报告结果与错误 code 完全一致。正确 code 会清空计数器。ticket 额度在拒绝时还会销毁 MFA ticket,客户端需从 Log in with a password 重新开始。
两种形态都不回答的额度(Allowances answering neither shape)
- Get desktop handoff information 与 Complete desktop handoff共享一个按客户端 IP 键控的失败尝试计数器:5 次失败会让两个操作被阻塞 15 分钟(自最近一次失败起算),被阻塞的请求返回
400 INVALID_HANDOFF_CODE(顶层 code,无校验条目)。Get desktop handoff information 还单独允许每个 handoff code 进行 3 次成功查询,第 4 次同样返回该顶层 code。 - Refund latest purchase允许每个账号每 30 天一次自助退款,窗口内的请求返回
403 STRIPE_REFUND_COOLDOWN_ACTIVE。 - Slowmode同样在处理器内强制执行。
慢速模式(Slowmode)
慢速模式限制一个账号在一个频道内发送消息的频率。Fluxer 将拒绝报告为普通请求失败:被拒绝的发送返回400 SLOWMODE_RATE_LIMITED,携带顶层小数秒retry_after与整秒Retry-After头。响应没有X-RateLimit-*头,客户端可通过状态码与 code 将其与桶拒绝区分。
- 额度为频道在
rate_limit_per_user中配置的每个时间间隔一条消息,按每个账号与频道对分别计数; - 仅对在 guild 频道中发送、且配置间隔大于零的非 Bot 账号计数;
- 持有 BYPASS_SLOWMODE 权限的调用者豁免;
- Get channel slowmode state 会报告调用者在尝试发送前的剩余延迟。
源码中,消息发送服务在发送前检查慢速模式:slowmodeKey = \slowmode:${channelId}:${user.id}`,并通过checkLimit检查([MessageSendService.ts](https://link.gitcode.com/i/e9cbedf17c4f07152c19798637a875fc));频道接口也暴露了get_channel_slowmode_state([ChannelController.ts](https://link.gitcode.com/i/ada8d5d0528cde497fe9e15a5bc82d7c))。测试 [SlowmodeEnforcement.test.ts](https://link.gitcode.com/i/69c8645d3876eef2b8ce1367836c5e1e) 验证了发送成功后才消耗慢速额度、以及超限返回400 SLOWMODE_RATE_LIMITED` 的行为。
其他协议面(Other surfaces)
每个协议面都有独立的限流契约:
- 主 Gateway:其会话、命令、重放(replay)、背压(backpressure)与准入(admission)限制见 Gateway limits and rate limits;
- Media Proxy API:没有请求计数限流,通过并发度、负载与截止时间限制约束工作;
- 上传中继(upload relay):每次传输都用会过期的上传 URL授权,并限制请求体大小。
小结:客户端与运维的实践要点
- 区分桶拒绝与处理器内拒绝:429 +
X-RateLimit-*头 +retry_after是桶拒绝;400SLOWMODE_RATE_LIMITED、400INVALID_FORM_BODY(带指向额度的校验码)等是处理器内限流。 - 用响应体
retry_after而不是响应头做退避:跨域客户端无法读取X-RateLimit-*头(见跨域策略)。 - 注意 429 会掩盖 401/403:凭证无效的请求也可能因限流返回 429,不要仅凭状态码判断认证失败。
- 全局拒绝会吊销用户会话:非 Bot 用户会话令牌被全局桶拒绝后必须重新认证;Bot 令牌与 OAuth2 令牌不受影响。
- 额度是持续补充的漏桶:耗尽后等待
retry_after(准入一个请求)或X-RateLimit-Reset-After(完全恢复)即可继续。 - IP 与子网键控:同一 IPv6
/64(或注册场景的/48子网)内的客户端共享额度,NAT/代理场景请留意共享影响。
围绕上述机制的完整配置聚合位于 RateLimitConfig.ts,它合并了 Auth、OAuth、User、Channel、Guild、Webhook、Admin 等 12 个模块的限流段;核心执行逻辑在 RateLimitMiddleware.ts,底层漏桶服务在 RateLimitService.ts,可供继续深入阅读。
【免费下载链接】fluxer
A free and open source instant messaging and VoIP chat app built for friends, groups, and communities.
相关推荐
如何快速上手DeepSeek-R1-Distill-Qwen-7B:AMD NPU部署的完整指南
如何快速上手DeepSeek R1 Distill Qwen 7B:AMD NPU部署的完整指南 DeepSeek R1 Distill Qwen 7B_rai
Apache Pulsar 限流重构(PIP-322):AsyncTokenBucket 无锁令牌桶与统一发布限流机制深度解析
Apache Pulsar 限流重构(PIP 322):AsyncTokenBucket 无锁令牌桶与统一发布限流机制深度解析 Apache Pulsar 的限
消息队列后端实战解析:5个高效使用Python通达信数据接口的核心技巧
实战解析:5个高效使用Python通达信数据接口的核心技巧 Python通达信数据接口为金融数据分析师和量化交易开发者提供了免费、高效且专业的数据获取解决方案。
金融科技数据分析
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考