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.tsdeliverPhoneOtp()

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

    抛的是普通 Error,不是 APIErrorsendPhoneInviteSms() 同理(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. 直接量:普通 Errorinstanceof APIErrorfalsenew APIError('TOO_MANY_REQUESTS').statusCode429

即:配额拒发 → 调用方拿到 HTTP 500,且响应体为 nullTOO_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.tsnormalizePhoneNumber 那样在本地写死这一个码并注明出处。

范围说明

本 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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions