From 20a741d54d32e316b5b3c73fb9e722ed509945d9 Mon Sep 17 00:00:00 2001 From: doublex001 <16157008+doublex001@user.noreply.gitee.com> Date: Wed, 30 Sep 2026 14:17:24 +0800 Subject: [PATCH] =?UTF-8?q?fix(http):=20=E9=94=99=E8=AF=AF=E5=93=8D?= =?UTF-8?q?=E5=BA=94=E7=BC=BA=E5=A4=B1=20data=20=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E6=97=B6=E9=80=8F=E4=BC=A0=E5=90=8E=E7=AB=AF=20message?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端错误响应是 {code,message,requestId,timestamp},无 data 字段; 旧代码用 isEnvelope(要求 data)判断错误体恒为 false,后端真实 message (如 sms rate limited / unsupported sms purpose)被 pickHttpMessage(400) 覆盖成笼统的「请求参数有误」,用户看不到验证码失败的真实原因 - 新增 extractError 从错误体提取 code/message,用于 !ok 与 401 分支 - 补回归测试:验证码限流(400/3005)应透传 sms rate limited --- src/__tests__/http.test.ts | 15 +++++++++++++++ src/api/http.ts | 28 ++++++++++++++++++++++------ 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/__tests__/http.test.ts b/src/__tests__/http.test.ts index 60a974f..eab977b 100644 --- a/src/__tests__/http.test.ts +++ b/src/__tests__/http.test.ts @@ -132,6 +132,21 @@ describe('信封 code 校验(规范 §3)', () => { } }); + it('HTTP 非 2xx 且错误体没有 data 字段时,仍透传后端 message(回归:验证码限流曾显示「请求参数有误」)', async () => { + // 真实后端错误体是 {code, message, requestId, timestamp},不含 data 字段; + // 旧实现用 isEnvelope(要求 data)判断,恒为 false,把后端 message 覆盖成「请求参数有误」 + responder = () => ({ status: 400, body: { code: 3005, message: 'sms rate limited', requestId: 'rid', timestamp: 't' } }); + try { + await request('/auth/sms/send', { method: 'POST', auth: false, query: { phone: '19900009021', purpose: 'login' } }); + throw new Error('不应成功'); + } catch (e) { + expect(e).toBeInstanceOf(ApiError); + expect((e as ApiError).httpStatus).toBe(400); + expect((e as ApiError).code).toBe(3005); + expect((e as ApiError).message).toBe('sms rate limited'); + } + }); + it('HTTP 非 2xx 且信封无 message 时,回退到状态码映射文案(§8)', async () => { responder = () => ({ status: 404, body: '' }); try { diff --git a/src/api/http.ts b/src/api/http.ts index 7046d66..4dd426e 100644 --- a/src/api/http.ts +++ b/src/api/http.ts @@ -143,6 +143,24 @@ function isEnvelope(value: unknown): value is ApiEnvelope { ); } +/** + * 从错误响应里提取业务 code 与 message。 + * + * 后端**错误**响应是 `{code, message, requestId, timestamp}`,**没有 `data` 字段**; + * 而 `isEnvelope` 要求 `data` 存在(那是给「成功响应解包」用的), + * 若拿它判断错误体,会恒为 false,导致后端真实 message 被 `pickHttpMessage` 覆盖成 + * 「请求参数有误」之类的笼统文案。故错误分支单独用本函数取值。 + */ +function extractError(parsed: unknown): { code: number | null; message: string | null } { + if (typeof parsed === 'object' && parsed !== null) { + const obj = parsed as Record; + const code = typeof obj.code === 'number' ? obj.code : null; + const message = typeof obj.message === 'string' && obj.message ? obj.message : null; + return { code, message }; + } + return { code: null, message: null }; +} + interface InternalOptions extends RequestOptions { /** 标记这是刷新请求本身,避免 401 时递归刷新 */ _isRefresh?: boolean; @@ -247,7 +265,7 @@ async function rawRequest(path: string, opts: InternalOptions): Promise { } throw new ApiError({ httpStatus: 401, - code: isEnvelope(parsed) ? parsed.code : 401, + code: extractError(parsed).code ?? 401, message: '登录已失效,请重新登录', requestId: resRequestId, }); @@ -255,13 +273,11 @@ async function rawRequest(path: string, opts: InternalOptions): Promise { // HTTP 状态码优先(§8) if (!response.ok) { - const msg = isEnvelope(parsed) - ? parsed.message - : pickHttpMessage(response.status); + const err = extractError(parsed); throw new ApiError({ httpStatus: response.status, - code: isEnvelope(parsed) ? parsed.code : response.status, - message: msg, + code: err.code ?? response.status, + message: err.message ?? pickHttpMessage(response.status), requestId: resRequestId, }); }