Skip to content

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

Closed

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. added
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    pm:queueReady for the PM dispatch loop
    on Aug 6, 2026
  2. 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

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Aug 7, 2026
  4. self-assigned this
    on Aug 7, 2026
  5. 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

  6. 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

Labels

pm:dispatchedDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch loop

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions