fix: handle Aliyun SMS validation errors
All checks were successful
CI/CD Pipeline / Unit Tests (push) Successful in 19s
CI/CD Pipeline / Build & Deploy Test (push) Has been skipped
CI/CD Pipeline / Build & Deploy Production (push) Successful in 1m18s

This commit is contained in:
Wang Zhuoxuan 2026-06-04 14:12:56 +08:00
parent 48cbc200d1
commit 0317c34099
2 changed files with 72 additions and 4 deletions

View File

@ -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('验证码无效或已过期');
});
});

View File

@ -11,6 +11,17 @@ const require = createRequire(import.meta.url);
const DypnsClient = require('@alicloud/dypnsapi20170525').default; const DypnsClient = require('@alicloud/dypnsapi20170525').default;
const { SendSmsVerifyCodeRequest, CheckSmsVerifyCodeRequest } = require('@alicloud/dypnsapi20170525/dist/models/model.js'); 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() { function createClient() {
const openApiConfig = new $OpenApiUtil.Config({ const openApiConfig = new $OpenApiUtil.Config({
accessKeyId: config.ALIYUN_ACCESS_KEY_ID, 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) { switch (code) {
case 'MOBILE_NUMBER_ILLEGAL': case 'MOBILE_NUMBER_ILLEGAL':
return new ValidationError('Invalid phone number'); 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 'BUSINESS_LIMIT_CONTROL':
case 'FREQUENCY_FAIL': case 'FREQUENCY_FAIL':
return new AppError('Too many SMS requests, please try again later', 429, 'RATE_LIMITED'); 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({ const RUNTIME_OPTIONS = new RuntimeOptions({
readTimeout: 10000, readTimeout: 10000,
connectTimeout: 5000, connectTimeout: 5000,
@ -70,12 +109,15 @@ export async function sendCode(phoneNumber: string): Promise<void> {
templateParam: config.ALIYUN_SMS_TEMPLATE_PARAM, 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') { if (!response.body || response.body.code !== 'OK') {
throw mapAliyunError( throw mapAliyunError(
response.body?.code ?? 'UNKNOWN', response.body?.code ?? 'UNKNOWN',
response.body?.message ?? 'Unknown SMS error', response.body?.message ?? 'Unknown SMS error',
'send',
); );
} }
} }
@ -89,16 +131,19 @@ export async function verifyCode(phoneNumber: string, code: string): Promise<voi
caseAuthPolicy: 1, caseAuthPolicy: 1,
}); });
const response = await createClient().checkSmsVerifyCodeWithOptions(request, RUNTIME_OPTIONS); const response = await createClient().checkSmsVerifyCodeWithOptions(request, RUNTIME_OPTIONS).catch((error: unknown) => {
throw mapAliyunException(error, 'verify');
});
if (!response.body || response.body.code !== 'OK') { if (!response.body || response.body.code !== 'OK') {
throw mapAliyunError( throw mapAliyunError(
response.body?.code ?? 'UNKNOWN', response.body?.code ?? 'UNKNOWN',
response.body?.message ?? 'Unknown SMS verification error', response.body?.message ?? 'Unknown SMS verification error',
'verify',
); );
} }
if (response.body.model?.verifyResult !== 'PASS') { if (response.body.model?.verifyResult !== 'PASS') {
throw new AppError('Invalid or expired verification code', 401, 'INVALID_VERIFY_CODE'); throw invalidVerifyCodeError();
} }
} }