Skip to content

fix(plugin-auth): 短信日配额拒发时 OTP 端点回 500 而非 429 —— deliverPhoneOtp 抛的是普通 Error #6039

Description

@hotlong

背景

#2814 把短信总量成本闸落在 SmsService.send()(service 层统一扣减点),超限时返回 SendSmsResult { status: 'failed', error: 'TOO_MANY_REQUESTS: daily SMS quota exhausted' }。诉求第 3 点要求 OTP / 邀请路径回 429 TOO_MANY_REQUESTS,与按号码冷却一致。

实测结论:这个语义到不了调用方,而且原因完全在 auth 端点侧,service 层无论怎么写都够不着。

实测证据

  1. packages/plugins/plugin-auth/src/auth-manager.ts 的 deliverPhoneOtp():

    if (result.status === 'failed') {
      throw new Error(`Phone OTP could not be sent: ${result.error ?? 'SMS delivery failed'}`);
    }
    

    抛的是普通 Error,不是 APIError。sendPhoneInviteSms() 同理(throw new Error('Invitation SMS failed: …'))。

  2. better-call@1.3.7(better-auth 的路由层)dist/router.mjs 第 93-98 行:

    if (isAPIError(error)) return toResponse(error);
    console.error(`# SERVER_ERROR: `, error);
    return new Response(null, { status: 500, statusText: "Internal Server Error" });
    

    isAPIError 的判定是 error instanceof APIError || error?.name === "APIError"(dist/utils.mjs:57)。

  3. 直接量:普通 Error 的 instanceof APIError 为 false;new APIError('TOO_MANY_REQUESTS').statusCode 为 429。

即:配额拒发 → 调用方拿到 HTTP 500,且响应体为 null(TOO_MANY_REQUESTS 只出现在服务端日志里)。同一端点上按号码闸走的是 hooks.before 里的 assertPhoneOtpSendAllowed(),它抛的是 APIError('TOO_MANY_REQUESTS'),正常回 429 —— 于是同一个端点上两道墙的对外表现不一致,正好是 #2814「两道墙从外面看应当一样」的反面。

建议修法(identity 车道)

在 deliverPhoneOtp / sendPhoneInviteSms 里识别 service 层信封上的 TOO_MANY_REQUESTS: 前缀,改抛 APIError('TOO_MANY_REQUESTS', …);其余失败仍按现状(传输故障回 500 是合理的)。文案沿用按号码闸的措辞,不泄露配额剩余细节。

@objectstack/service-sms 已导出常量 SMS_QUOTA_EXCEEDED_CODE / SMS_QUOTA_EXCEEDED_ERROR 供比对,但 plugin-auth 依赖 service-sms 会造成反向依赖 —— 更可能的做法是像 otp-send-guard.ts 对 normalizePhoneNumber 那样在本地写死这一个码并注明出处。

范围说明

本 issue 从 #2814 的实现中分出:#2814 的文件面被限定在 packages/services/**,⛔ 不动 plugin-auth,所以此处如实记录而非顺手改。#2814 的 PR 会写明「OTP 路径当前得到 500」这一事实。

Blocked-by: #2814
(#2814 的实现 PR #6042 仍 open —— service 层的 TOO_MANY_REQUESTS: 失败信封尚未落 main,本单要识别的正是那个前缀。分诊 2026-08-06 于 origin/main 9e3709a 核过:SMS_QUOTA_EXCEEDED_CODE / daily SMS quota 零命中,阳性对照 packages/services/service-sms 目录存在。)


Generated by Claude Code

Activity

  1. claude commented on Aug 6, 2026

    @claude
    Contributor

    分诊:pm:queue + pm:blocked(Blocked-by: #2814)+ domain:identity

    落点锚定:packages/plugins/plugin-auth/src/auth-manager.ts —— deliverPhoneOtp() 定义在 :2808、抛普通 Error 在 :2840,两个调用点在 :2228 / :2236(origin/main 9e3709a 逐行核过)⇒ packages/plugins/plugin-auth ⇒ domain:identity。⛔ 不按「短信配额」词汇归 services:配额闸在 services,但本单要改的那一行在 plugin-auth。

    过时前提检查两条(一条正、一条反)

    1. 正:auth-manager.ts 自 7f1a635 之后无提交触及 ⇒ 正文引用的三处仍成立。
    2. 反(即本单被阻的理由):origin/main 上 SMS_QUOTA_EXCEEDED_CODE 与 daily SMS quota 零命中(阳性对照:packages/services/service-sms 目录确实存在)—— 也就是说本单要识别的 service 层 TOO_MANY_REQUESTS: 失败信封还没落地,它由仍 open 的 PR feat(sms): 短信全局日发送配额 —— 成本总量闸 (#2814) #6042(feat(sms): 短信全局/每租户日发送配额(成本总量闸) #2814)引入。先做等于去钉一个尚不存在的字符串 ⇒ 已加 pm:blocked,并在正文追加 Blocked-by: #2814 机器半边。

    串行约束(硬,identity 车道请勿越过):在飞 PR #6010(#5942,/sso/register 等级尺)实测 diff 命中 同一个 auth-manager.ts(及其 .test.ts);另有 #5941(最后一个管理员删除路径守卫,将新建 last-admin-guard.ts)与 #5978(break-glass 第三条路径)落在同一区域。⇒ 解阻后开工前必须重拉 origin/main,并与上述单按单串行。

    分类理由(入队而非决策箱):修法方向不改公开契约 —— 429 本就是 #2814 诉求第 3 点已确立的目标,同端点上按号码闸走的 assertPhoneOtpSendAllowed() 已经在回 429,本单只是把第二道墙的对外表现对齐到第一道墙。正文给出的「本地写死一个码 + 注明出处」也已有 otp-send-guard.ts 的同仓先例 ⇒ 无须维护者拍板。

    查重:三仓 open issue 搜 TOO_MANY_REQUESTS / OTP / APIError 仅命中本单 ⇒ 无第二个派发入口;#2814 是父单(实现面被限定在 packages/services/**,本单是其如实分出的 plugin-auth 半边,非重复)。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. self-assigned this
    on Aug 7, 2026
  3. baozhoutao commented on Aug 7, 2026

    @baozhoutao
    Contributor

    解锁 + 认领(identity 车道 PM,session_01JwwiU9bjhwy2SWj13ho8uv,第 11 轮,2026-08-07 00:4xZ)

    Blocked-by: #2814 已解除 —— 实现 PR #6042 于 23:56:04Z 合入 main(9c90ea0b7)。按分诊留下的对照查询实测复核:SMS_QUOTA_EXCEEDED_CODE / daily SMS quota 在 origin/main 的 packages/services/service-sms 现有 4 文件命中(此前零命中),失败信封已存在;本单要改的两处普通 Error 抛点仍在(auth-manager.ts:2844 / :2873)。摘 pm:blocked。


    Generated by Claude Code

  4. baozhoutao commented on Aug 7, 2026

    @baozhoutao
    Contributor

    收取 → 审查:ACCEPT(identity 车道 PM,session_01JwwiU9bjhwy2SWj13ho8uv,第 11 轮,01:1xZ)→ PR #6092 翻 ready + 挂 auto-merge。

    审查基础(全量 diff,3 文件 +280/−2):

    零返工纪录保持(十一轮 20 单)。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions