From 0317c34099eab7db6208b5d280a7c870335aa7a5 Mon Sep 17 00:00:00 2001 From: Wang Zhuoxuan Date: Thu, 4 Jun 2026 14:12:56 +0800 Subject: [PATCH] fix: handle Aliyun SMS validation errors --- .../services/auth/aliyun-sms.test.ts | 23 ++++++++ src/services/auth/aliyun-sms.ts | 53 +++++++++++++++++-- 2 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 src/__tests__/services/auth/aliyun-sms.test.ts diff --git a/src/__tests__/services/auth/aliyun-sms.test.ts b/src/__tests__/services/auth/aliyun-sms.test.ts new file mode 100644 index 0000000..7349b5c --- /dev/null +++ b/src/__tests__/services/auth/aliyun-sms.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest'; +import { mapAliyunException } from '../../../services/auth/aliyun-sms.js'; + +describe('aliyun-sms', () => { + it('maps Aliyun validate failure during verification to invalid code error', () => { + const error = { + name: 'ClientError', + code: 'isv.ValidateFail', + message: 'isv.ValidateFail: code: 400, 验证失败 request id: request-id', + data: { + Code: 'isv.ValidateFail', + Message: '验证失败', + }, + statusCode: 400, + }; + + const mapped = mapAliyunException(error, 'verify'); + + expect(mapped.statusCode).toBe(401); + expect(mapped.code).toBe('INVALID_VERIFY_CODE'); + expect(mapped.message).toBe('验证码无效或已过期'); + }); +}); diff --git a/src/services/auth/aliyun-sms.ts b/src/services/auth/aliyun-sms.ts index c965078..c98f443 100644 --- a/src/services/auth/aliyun-sms.ts +++ b/src/services/auth/aliyun-sms.ts @@ -11,6 +11,17 @@ const require = createRequire(import.meta.url); const DypnsClient = require('@alicloud/dypnsapi20170525').default; const { SendSmsVerifyCodeRequest, CheckSmsVerifyCodeRequest } = require('@alicloud/dypnsapi20170525/dist/models/model.js'); +type SmsOperation = 'send' | 'verify'; + +type AliyunSdkError = { + code?: unknown; + message?: unknown; + data?: { + Code?: unknown; + Message?: unknown; + }; +}; + function createClient() { const openApiConfig = new $OpenApiUtil.Config({ accessKeyId: config.ALIYUN_ACCESS_KEY_ID, @@ -31,10 +42,22 @@ function assertSmsConfigured(): void { } } -function mapAliyunError(code: string, message: string): AppError { +function getString(value: unknown): string | undefined { + return typeof value === 'string' && value.length > 0 ? value : undefined; +} + +function invalidVerifyCodeError(): AppError { + return new AppError('验证码无效或已过期', 401, 'INVALID_VERIFY_CODE'); +} + +function mapAliyunError(code: string, message: string, operation: SmsOperation): AppError { switch (code) { case 'MOBILE_NUMBER_ILLEGAL': return new ValidationError('Invalid phone number'); + case 'isv.ValidateFail': + return operation === 'verify' + ? invalidVerifyCodeError() + : new AppError('SMS service error: verification failed', 502, 'SMS_SERVICE_ERROR'); case 'BUSINESS_LIMIT_CONTROL': case 'FREQUENCY_FAIL': return new AppError('Too many SMS requests, please try again later', 429, 'RATE_LIMITED'); @@ -55,6 +78,22 @@ function mapAliyunError(code: string, message: string): AppError { } } +export function mapAliyunException(error: unknown, operation: SmsOperation): AppError { + if (error instanceof AppError) return error; + + if (typeof error === 'object' && error !== null) { + const sdkError = error as AliyunSdkError; + const code = getString(sdkError.data?.Code) ?? getString(sdkError.code); + const message = getString(sdkError.data?.Message) ?? getString(sdkError.message); + + if (code) { + return mapAliyunError(code, message ?? code, operation); + } + } + + return new AppError('SMS service error: request failed', 502, 'SMS_SERVICE_ERROR'); +} + const RUNTIME_OPTIONS = new RuntimeOptions({ readTimeout: 10000, connectTimeout: 5000, @@ -70,12 +109,15 @@ export async function sendCode(phoneNumber: string): Promise { templateParam: config.ALIYUN_SMS_TEMPLATE_PARAM, }); - const response = await createClient().sendSmsVerifyCodeWithOptions(request, RUNTIME_OPTIONS); + const response = await createClient().sendSmsVerifyCodeWithOptions(request, RUNTIME_OPTIONS).catch((error: unknown) => { + throw mapAliyunException(error, 'send'); + }); if (!response.body || response.body.code !== 'OK') { throw mapAliyunError( response.body?.code ?? 'UNKNOWN', response.body?.message ?? 'Unknown SMS error', + 'send', ); } } @@ -89,16 +131,19 @@ export async function verifyCode(phoneNumber: string, code: string): Promise { + throw mapAliyunException(error, 'verify'); + }); if (!response.body || response.body.code !== 'OK') { throw mapAliyunError( response.body?.code ?? 'UNKNOWN', response.body?.message ?? 'Unknown SMS verification error', + 'verify', ); } if (response.body.model?.verifyResult !== 'PASS') { - throw new AppError('Invalid or expired verification code', 401, 'INVALID_VERIFY_CODE'); + throw invalidVerifyCodeError(); } }