0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-10 22:22:45 -05:00

feat(connector): connector error handler, throw errmsg on general errors (#1458)

This commit is contained in:
Darcy Ye 2022-07-08 21:11:31 +08:00 committed by GitHub
parent da882cee85
commit 7da1de33e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 293 additions and 167 deletions

View file

@ -11,6 +11,10 @@ export const alipaySigningAlgorithmMapping = {
} as const;
export const alipaySigningAlgorithms = ['RSA', 'RSA2'] as const;
export const invalidAccessTokenCode = ['20001'];
export const invalidAccessTokenSubCode = ['isv.code-invalid'];
export const defaultMetadata: ConnectorMetadata = {
id: 'alipay-native',
target: 'alipay',

View file

@ -216,7 +216,12 @@ describe('getUserInfo', () => {
});
await expect(alipayNativeMethods.getUserInfo({ auth_code: 'wrong_code' })).rejects.toMatchError(
new ConnectorError(ConnectorErrorCodes.General, 'Invalid parameter')
new ConnectorError(ConnectorErrorCodes.General, {
errorDescription: 'Invalid parameter',
code: '40002',
sub_code: 'isv.invalid-parameter',
sub_msg: '参数无效',
})
);
});

View file

@ -30,6 +30,8 @@ import {
defaultMetadata,
defaultTimeout,
timestampFormat,
invalidAccessTokenCode,
invalidAccessTokenSubCode,
} from './constant';
import {
alipayNativeConfigGuard,
@ -139,28 +141,35 @@ export default class AlipayNativeConnector implements SocialConnector {
const { alipay_user_info_share_response } = result.data;
const {
user_id: id,
avatar,
nick_name: name,
code,
msg,
sub_code,
} = alipay_user_info_share_response;
this.errorHandler(alipay_user_info_share_response);
this.errorHandler({ code, msg, sub_code });
assert(id, new ConnectorError(ConnectorErrorCodes.InvalidResponse));
const { user_id: id, avatar, nick_name: name } = alipay_user_info_share_response;
if (!id) {
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse);
}
return { id, avatar, name };
};
private readonly errorHandler: ErrorHandler = ({ code, msg, sub_code }) => {
assert(code !== '20001', new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, msg));
assert(
sub_code !== 'isv.code-invalid',
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, msg)
);
assert(!sub_code, new ConnectorError(ConnectorErrorCodes.General, msg));
private readonly errorHandler: ErrorHandler = ({ code, msg, sub_code, sub_msg }) => {
if (invalidAccessTokenCode.includes(code)) {
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, msg);
}
if (sub_code) {
assert(
!invalidAccessTokenSubCode.includes(sub_code),
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, msg)
);
throw new ConnectorError(ConnectorErrorCodes.General, {
errorDescription: msg,
code,
sub_code,
sub_msg,
});
}
};
private readonly authorizationCallbackHandler = async (parameterObject: unknown) => {

View file

@ -47,6 +47,8 @@ export const alipayUserInfoShareResponseGuard = z.object({
sub_msg: z.string().optional(),
});
type AlipayUserInfoShareResponseGuard = z.infer<typeof alipayUserInfoShareResponseGuard>;
export const userInfoResponseGuard = z.object({
sign: z.string(), // To know `sign` details, see: https://opendocs.alipay.com/common/02kf5q
alipay_user_info_share_response: alipayUserInfoShareResponseGuard,
@ -54,9 +56,4 @@ export const userInfoResponseGuard = z.object({
export type UserInfoResponse = z.infer<typeof userInfoResponseGuard>;
export type ErrorHandler = (response: {
code: string;
msg: string;
sub_code?: string;
sub_msg?: string;
}) => void;
export type ErrorHandler = (response: AlipayUserInfoShareResponseGuard) => void;

View file

@ -14,6 +14,10 @@ export const alipaySigningAlgorithms = ['RSA', 'RSA2'] as const;
export const charsetEnum = ['GBK', 'UTF8'] as const;
export const fallbackCharset = 'UTF8';
export const invalidAccessTokenCode = ['20001'];
export const invalidAccessTokenSubCode = ['isv.code-invalid'];
export const defaultMetadata: ConnectorMetadata = {
id: 'alipay-web',
target: 'alipay',

View file

@ -159,7 +159,7 @@ describe('getUserInfo', () => {
it('throw General error if auth_code not provided in input', async () => {
await expect(alipayMethods.getUserInfo({})).rejects.toMatchError(
new ConnectorError(ConnectorErrorCodes.General, '{}')
new ConnectorError(ConnectorErrorCodes.InvalidResponse, '{}')
);
});
@ -216,7 +216,12 @@ describe('getUserInfo', () => {
});
await expect(alipayMethods.getUserInfo({ auth_code: 'wrong_code' })).rejects.toMatchError(
new ConnectorError(ConnectorErrorCodes.General, 'Invalid parameter')
new ConnectorError(ConnectorErrorCodes.General, {
errorDescription: 'Invalid parameter',
code: '40002',
sub_code: 'isv.invalid-parameter',
sub_msg: '参数无效',
})
);
});

View file

@ -30,6 +30,8 @@ import {
defaultTimeout,
timestampFormat,
fallbackCharset,
invalidAccessTokenCode,
invalidAccessTokenSubCode,
} from './constant';
import {
alipayConfigGuard,
@ -140,7 +142,9 @@ export default class AlipayConnector implements SocialConnector {
timeout: defaultTimeout,
});
const result = userInfoResponseGuard.safeParse(JSON.parse(httpResponse.body));
const { body: rawBody } = httpResponse;
const result = userInfoResponseGuard.safeParse(JSON.parse(rawBody));
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error.message);
@ -148,28 +152,35 @@ export default class AlipayConnector implements SocialConnector {
const { alipay_user_info_share_response } = result.data;
const {
user_id: id,
avatar,
nick_name: name,
code,
msg,
sub_code,
} = alipay_user_info_share_response;
this.errorHandler(alipay_user_info_share_response);
this.errorHandler({ code, msg, sub_code });
assert(id, new ConnectorError(ConnectorErrorCodes.InvalidResponse));
const { user_id: id, avatar, nick_name: name } = alipay_user_info_share_response;
if (!id) {
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse);
}
return { id, avatar, name };
};
private readonly errorHandler: ErrorHandler = ({ code, msg, sub_code }) => {
assert(code !== '20001', new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, msg));
assert(
sub_code !== 'isv.code-invalid',
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, msg)
);
assert(!sub_code, new ConnectorError(ConnectorErrorCodes.General, msg));
private readonly errorHandler: ErrorHandler = ({ code, msg, sub_code, sub_msg }) => {
if (invalidAccessTokenCode.includes(code)) {
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, msg);
}
if (sub_code) {
assert(
!invalidAccessTokenSubCode.includes(sub_code),
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, msg)
);
throw new ConnectorError(ConnectorErrorCodes.General, {
errorDescription: msg,
code,
sub_code,
sub_msg,
});
}
};
private readonly authorizationCallbackHandler = async (parameterObject: unknown) => {
@ -178,7 +189,10 @@ export default class AlipayConnector implements SocialConnector {
const result = dataGuard.safeParse(parameterObject);
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(parameterObject));
throw new ConnectorError(
ConnectorErrorCodes.InvalidResponse,
JSON.stringify(parameterObject)
);
}
return result.data;

View file

@ -48,6 +48,8 @@ export const alipayUserInfoShareResponseGuard = z.object({
sub_msg: z.string().optional(),
});
type AlipayUserInfoShareResponse = z.infer<typeof alipayUserInfoShareResponseGuard>;
export const userInfoResponseGuard = z.object({
sign: z.string(), // To know `sign` details, see: https://opendocs.alipay.com/common/02kf5q
alipay_user_info_share_response: alipayUserInfoShareResponseGuard,
@ -55,9 +57,4 @@ export const userInfoResponseGuard = z.object({
export type UserInfoResponse = z.infer<typeof userInfoResponseGuard>;
export type ErrorHandler = (response: {
code: string;
msg: string;
sub_code?: string;
sub_msg?: string;
}) => void;
export type ErrorHandler = (response: AlipayUserInfoShareResponse) => void;

View file

@ -78,12 +78,13 @@ export default class AliyunDmConnector implements EmailConnector {
const {
response: { body: rawBody },
} = error;
assert(
typeof rawBody === 'string',
new ConnectorError(ConnectorErrorCodes.InvalidResponse)
);
this.errorHandler(rawBody);
throw new ConnectorError(ConnectorErrorCodes.General, rawBody);
}
throw error;
@ -97,17 +98,8 @@ export default class AliyunDmConnector implements EmailConnector {
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error.message);
}
const { Code } = result.data;
const { Message: errorDescription, ...rest } = result.data;
// See https://help.aliyun.com/document_detail/29444.html.
assert(
!(
Code === 'InvalidBody' ||
Code === 'InvalidTemplate.NotFound' ||
Code === 'InvalidSubject.Malformed' ||
Code === 'InvalidFromAlias.Malformed'
),
new ConnectorError(ConnectorErrorCodes.InvalidConfig, errorResponseBody)
);
throw new ConnectorError(ConnectorErrorCodes.General, { errorDescription, ...rest });
};
}

View file

@ -63,6 +63,7 @@ export const sendMailErrorResponseGuard = z.object({
Message: z.string(),
RequestId: z.string().optional(),
HostId: z.string().optional(),
Recommend: z.string().optional(),
});
export type SendMailErrorResponse = z.infer<typeof sendMailErrorResponseGuard>;

View file

@ -27,7 +27,6 @@ export default class AliyunSmsConnector implements SmsConnector {
}
};
/* eslint-disable complexity */
public sendMessage: SmsSendMessageFunction = async (phone, type, { code }, config) => {
const smsConfig =
(config as AliyunSmsConfig | undefined) ?? (await this.getConfig(this.metadata.id));
@ -54,14 +53,14 @@ export default class AliyunSmsConnector implements SmsConnector {
const { body: rawBody } = httpResponse;
const { Code } = this.parseResponseString(rawBody);
if (Code === 'isv.ACCOUNT_NOT_EXISTS' || Code === 'isv.SMS_TEMPLATE_ILLEGAL') {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, rawBody);
}
const { Code, Message, ...rest } = this.parseResponseString(rawBody);
if (Code !== 'OK') {
throw new ConnectorError(ConnectorErrorCodes.General, rawBody);
throw new ConnectorError(ConnectorErrorCodes.General, {
errorDescription: Message,
Code,
...rest,
});
}
return httpResponse;
@ -76,20 +75,15 @@ export default class AliyunSmsConnector implements SmsConnector {
assert(typeof rawBody === 'string', new ConnectorError(ConnectorErrorCodes.InvalidResponse));
const { Code } = this.parseResponseString(rawBody);
const { Code, Message, ...rest } = this.parseResponseString(rawBody);
if (Code.includes('InvalidAccessKeyId')) {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, rawBody);
}
if (Code === 'SignatureDoesNotMatch' || Code === 'IncompleteSignature') {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, rawBody);
}
throw new ConnectorError(ConnectorErrorCodes.General, rawBody);
throw new ConnectorError(ConnectorErrorCodes.General, {
errorDescription: Message,
Code,
...rest,
});
}
};
/* eslint-enable complexity */
private readonly parseResponseString = (response: string) => {
const result = sendSmsResponseGuard.safeParse(JSON.parse(response));

View file

@ -194,10 +194,12 @@ describe('facebook connector', () => {
error_reason: 'user_denied',
})
).rejects.toMatchError(
new ConnectorError(
ConnectorErrorCodes.General,
'{"error":"general_error","error_code":200,"error_description":"General error encountered.","error_reason":"user_denied"}'
)
new ConnectorError(ConnectorErrorCodes.General, {
error: 'general_error',
error_code: 200,
errorDescription: 'General error encountered.',
error_reason: 'user_denied',
})
);
});

View file

@ -15,7 +15,7 @@ import {
codeWithRedirectDataGuard,
} from '@logto/connector-types';
import { assert } from '@silverhand/essentials';
import got, { RequestError as GotRequestError } from 'got';
import got, { HTTPError } from 'got';
import {
accessTokenEndpoint,
@ -118,9 +118,16 @@ export default class FacebookConnector implements SocialConnector {
name,
};
} catch (error: unknown) {
if (error instanceof GotRequestError && error.response?.statusCode === 400) {
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid);
if (error instanceof HTTPError) {
const { statusCode, body: rawBody } = error.response;
if (statusCode === 400) {
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid);
}
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(rawBody));
}
throw error;
}
};
@ -135,16 +142,23 @@ export default class FacebookConnector implements SocialConnector {
const parsedError = authorizationCallbackErrorGuard.safeParse(parameterObject);
if (!parsedError.success) {
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(parameterObject));
}
if (parsedError.data.error === 'access_denied') {
throw new ConnectorError(
ConnectorErrorCodes.AuthorizationFailed,
parsedError.data.error_description
ConnectorErrorCodes.InvalidResponse,
JSON.stringify(parameterObject)
);
}
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(parameterObject));
const { error, error_code, error_description, error_reason } = parsedError.data;
if (error === 'access_denied') {
throw new ConnectorError(ConnectorErrorCodes.AuthorizationFailed, error_description);
}
throw new ConnectorError(ConnectorErrorCodes.General, {
error,
error_code,
errorDescription: error_description,
error_reason,
});
};
}

View file

@ -10,7 +10,7 @@ import {
codeDataGuard,
} from '@logto/connector-types';
import { assert, conditional } from '@silverhand/essentials';
import got, { RequestError as GotRequestError } from 'got';
import got, { HTTPError } from 'got';
import * as qs from 'query-string';
import {
@ -110,9 +110,16 @@ export default class GithubConnector implements SocialConnector {
name: conditional(name),
};
} catch (error: unknown) {
if (error instanceof GotRequestError && error.response?.statusCode === 401) {
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid);
if (error instanceof HTTPError) {
const { statusCode, body: rawBody } = error.response;
if (statusCode === 401) {
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid);
}
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(rawBody));
}
throw error;
}
};
@ -130,13 +137,16 @@ export default class GithubConnector implements SocialConnector {
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(parameterObject));
}
if (parsedError.data.error === 'access_denied') {
throw new ConnectorError(
ConnectorErrorCodes.AuthorizationFailed,
parsedError.data.error_description
);
const { error, error_description, error_uri } = parsedError.data;
if (error === 'access_denied') {
throw new ConnectorError(ConnectorErrorCodes.AuthorizationFailed, error_description);
}
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(parameterObject));
throw new ConnectorError(ConnectorErrorCodes.General, {
error,
errorDescription: error_description,
error_uri,
});
};
}

View file

@ -14,7 +14,7 @@ import {
codeWithRedirectDataGuard,
} from '@logto/connector-types';
import { conditional, assert } from '@silverhand/essentials';
import got, { RequestError as GotRequestError } from 'got';
import got, { HTTPError } from 'got';
import {
accessTokenEndpoint,
@ -87,6 +87,7 @@ export default class GoogleConnector implements SocialConnector {
return { accessToken };
};
// eslint-disable-next-line complexity
public getUserInfo: GetUserInfo = async (data) => {
const { code, redirectUri } = await this.authorizationCallbackHandler(data);
const { accessToken } = await this.getAccessToken(code, redirectUri);
@ -104,6 +105,7 @@ export default class GoogleConnector implements SocialConnector {
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, result.error.message);
}
const { sub: id, picture: avatar, email, email_verified, name } = result.data;
return {
@ -113,10 +115,15 @@ export default class GoogleConnector implements SocialConnector {
name,
};
} catch (error: unknown) {
assert(
!(error instanceof GotRequestError && error.response?.statusCode === 401),
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)
);
if (error instanceof HTTPError) {
const { statusCode, body: rawBody } = error.response;
if (statusCode === 401) {
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid);
}
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(rawBody));
}
throw error;
}

View file

@ -62,11 +62,12 @@ export type SendSmsResponse = {
uri: string;
};
// See https://www.twilio.com/docs/usage/twilios-response
export const sendSmsErrorResponseGuard = z.object({
code: z.number(),
message: z.string(),
more_info: z.string(),
status: z.number(),
message: z.string(),
code: z.number().optional(),
more_info: z.string().optional(),
});
export type SendSmsErrorResponse = z.infer<typeof sendSmsErrorResponseGuard>;

View file

@ -5,6 +5,11 @@ export const accessTokenEndpoint = 'https://api.weixin.qq.com/sns/oauth2/access_
export const userInfoEndpoint = 'https://api.weixin.qq.com/sns/userinfo';
export const scope = 'snsapi_userinfo';
// See https://developers.weixin.qq.com/doc/oplatform/Return_codes/Return_code_descriptions_new.html to know more about WeChat response error code
export const invalidAuthCodeErrcode = [40_029, 40_163, 42_003];
export const invalidAccessTokenErrcode = [40_001, 40_014];
export const defaultMetadata: ConnectorMetadata = {
id: 'wechat-native',
target: 'wechat',

View file

@ -83,7 +83,10 @@ describe('getAccessToken', () => {
.query(true)
.reply(200, { errcode: -1, errmsg: 'system error' });
await expect(wechatNativeMethods.getAccessToken('wrong_code')).rejects.toMatchError(
new ConnectorError(ConnectorErrorCodes.General, 'system error')
new ConnectorError(ConnectorErrorCodes.General, {
errorDescription: 'system error',
errcode: -1,
})
);
});
});
@ -175,7 +178,10 @@ describe('getUserInfo', () => {
errmsg: 'missing openid',
});
await expect(wechatNativeMethods.getUserInfo({ code: 'code' })).rejects.toMatchError(
new Error('missing openid')
new ConnectorError(ConnectorErrorCodes.General, {
errorDescription: 'missing openid',
errcode: 41_009,
})
);
});
@ -200,7 +206,10 @@ describe('getUserInfo', () => {
.query(parameters)
.reply(200, { errcode: 40_003, errmsg: 'invalid openid' });
await expect(wechatNativeMethods.getUserInfo({ code: 'code' })).rejects.toMatchError(
new Error('invalid openid')
new ConnectorError(ConnectorErrorCodes.General, {
errorDescription: 'invalid openid',
errcode: 40_003,
})
);
});

View file

@ -15,7 +15,7 @@ import {
codeDataGuard,
} from '@logto/connector-types';
import { assert } from '@silverhand/essentials';
import got, { RequestError as GotRequestError } from 'got';
import got, { HTTPError } from 'got';
import {
authorizationEndpoint,
@ -23,6 +23,8 @@ import {
userInfoEndpoint,
defaultMetadata,
defaultTimeout,
invalidAccessTokenErrcode,
invalidAuthCodeErrcode,
} from './constant';
import {
wechatNativeConfigGuard,
@ -84,6 +86,7 @@ export default class WechatNativeConnector implements SocialConnector {
return { accessToken, openid };
};
// eslint-disable-next-line complexity
public getUserInfo: GetUserInfo = async (data) => {
const { code } = await this.authorizationCallbackHandler(data);
const { accessToken, openid } = await this.getAccessToken(code);
@ -109,10 +112,15 @@ export default class WechatNativeConnector implements SocialConnector {
return { id: unionid ?? openid, avatar: headimgurl, name: nickname };
} catch (error: unknown) {
assert(
!(error instanceof GotRequestError && error.response?.statusCode === 401),
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)
);
if (error instanceof HTTPError) {
const { statusCode, body: rawBody } = error.response;
if (statusCode === 401) {
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid);
}
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(rawBody));
}
throw error;
}
@ -121,28 +129,28 @@ export default class WechatNativeConnector implements SocialConnector {
// See https://developers.weixin.qq.com/doc/oplatform/Return_codes/Return_code_descriptions_new.html
private readonly getAccessTokenErrorHandler: GetAccessTokenErrorHandler = (accessToken) => {
const { errcode, errmsg } = accessToken;
assert(
errcode !== 40_029,
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, errmsg)
);
assert(
errcode !== 40_163,
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, errmsg)
);
assert(
errcode !== 42_003,
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, errmsg)
);
assert(!errcode, new ConnectorError(ConnectorErrorCodes.General, errmsg));
if (errcode) {
assert(
!invalidAuthCodeErrcode.includes(errcode),
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, errmsg)
);
throw new ConnectorError(ConnectorErrorCodes.General, { errorDescription: errmsg, errcode });
}
};
private readonly getUserInfoErrorHandler: GetUserInfoErrorHandler = (userInfo) => {
const { errcode, errmsg } = userInfo;
assert(
!(errcode === 40_001 || errcode === 40_014),
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, errmsg)
);
assert(!errcode, new Error(errmsg ?? ''));
if (errcode) {
assert(
!invalidAccessTokenErrcode.includes(errcode),
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, errmsg)
);
throw new ConnectorError(ConnectorErrorCodes.General, { errorDescription: errmsg, errcode });
}
};
private readonly authorizationCallbackHandler = async (parameterObject: unknown) => {

View file

@ -5,6 +5,11 @@ export const accessTokenEndpoint = 'https://api.weixin.qq.com/sns/oauth2/access_
export const userInfoEndpoint = 'https://api.weixin.qq.com/sns/userinfo';
export const scope = 'snsapi_login';
// See https://developers.weixin.qq.com/doc/oplatform/Return_codes/Return_code_descriptions_new.html to know more about WeChat response error code
export const invalidAuthCodeErrcode = [40_029, 40_163, 42_003];
export const invalidAccessTokenErrcode = [40_001, 40_014];
export const defaultMetadata: ConnectorMetadata = {
id: 'wechat-web',
target: 'wechat',

View file

@ -83,7 +83,10 @@ describe('getAccessToken', () => {
.query(true)
.reply(200, { errcode: -1, errmsg: 'system error' });
await expect(wechatMethods.getAccessToken('wrong_code')).rejects.toMatchError(
new ConnectorError(ConnectorErrorCodes.General, 'system error')
new ConnectorError(ConnectorErrorCodes.General, {
errorDescription: 'system error',
errcode: -1,
})
);
});
});
@ -168,7 +171,10 @@ describe('getUserInfo', () => {
errmsg: 'missing openid',
});
await expect(wechatMethods.getUserInfo({ code: 'code' })).rejects.toMatchError(
new Error('missing openid')
new ConnectorError(ConnectorErrorCodes.General, {
errorDescription: 'missing openid',
errcode: 41_009,
})
);
});
@ -193,7 +199,10 @@ describe('getUserInfo', () => {
.query(parameters)
.reply(200, { errcode: 40_003, errmsg: 'invalid openid' });
await expect(wechatMethods.getUserInfo({ code: 'code' })).rejects.toMatchError(
new Error('invalid openid')
new ConnectorError(ConnectorErrorCodes.General, {
errorDescription: 'invalid openid',
errcode: 40_003,
})
);
});

View file

@ -15,7 +15,7 @@ import {
codeDataGuard,
} from '@logto/connector-types';
import { assert } from '@silverhand/essentials';
import got, { RequestError as GotRequestError } from 'got';
import got, { HTTPError } from 'got';
import {
authorizationEndpoint,
@ -24,6 +24,8 @@ import {
scope,
defaultMetadata,
defaultTimeout,
invalidAccessTokenErrcode,
invalidAuthCodeErrcode,
} from './constant';
import {
wechatConfigGuard,
@ -80,11 +82,13 @@ export default class WechatConnector implements SocialConnector {
const { access_token: accessToken, openid } = result.data;
this.getAccessTokenErrorHandler(result.data);
assert(accessToken && openid, new ConnectorError(ConnectorErrorCodes.InvalidResponse));
return { accessToken, openid };
};
// eslint-disable-next-line complexity
public getUserInfo: GetUserInfo = async (data) => {
const { code } = await this.authorizationCallbackHandler(data);
const { accessToken, openid } = await this.getAccessToken(code);
@ -110,10 +114,15 @@ export default class WechatConnector implements SocialConnector {
return { id: unionid ?? openid, avatar: headimgurl, name: nickname };
} catch (error: unknown) {
assert(
!(error instanceof GotRequestError && error.response?.statusCode === 401),
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid)
);
if (error instanceof HTTPError) {
const { statusCode, body: rawBody } = error.response;
if (statusCode === 401) {
throw new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid);
}
throw new ConnectorError(ConnectorErrorCodes.General, JSON.stringify(rawBody));
}
throw error;
}
@ -122,28 +131,28 @@ export default class WechatConnector implements SocialConnector {
// See https://developers.weixin.qq.com/doc/oplatform/Return_codes/Return_code_descriptions_new.html
private readonly getAccessTokenErrorHandler: GetAccessTokenErrorHandler = (accessToken) => {
const { errcode, errmsg } = accessToken;
assert(
errcode !== 40_029,
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, errmsg)
);
assert(
errcode !== 40_163,
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, errmsg)
);
assert(
errcode !== 42_003,
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, errmsg)
);
assert(!errcode, new ConnectorError(ConnectorErrorCodes.General, errmsg));
if (errcode) {
assert(
!invalidAuthCodeErrcode.includes(errcode),
new ConnectorError(ConnectorErrorCodes.SocialAuthCodeInvalid, errmsg)
);
throw new ConnectorError(ConnectorErrorCodes.General, { errorDescription: errmsg, errcode });
}
};
private readonly getUserInfoErrorHandler: GetUserInfoErrorHandler = (userInfo) => {
const { errcode, errmsg } = userInfo;
assert(
!(errcode === 40_001 || errcode === 40_014),
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, errmsg)
);
assert(!errcode, new Error(errmsg ?? ''));
if (errcode) {
assert(
!invalidAccessTokenErrcode.includes(errcode),
new ConnectorError(ConnectorErrorCodes.SocialAccessTokenInvalid, errmsg)
);
throw new ConnectorError(ConnectorErrorCodes.General, { errorDescription: errmsg, errcode });
}
};
private readonly authorizationCallbackHandler = async (parameterObject: unknown) => {

View file

@ -167,7 +167,7 @@ describe('koaConnectorErrorHandler middleware', () => {
);
});
it('General connector errors', async () => {
it('General connector errors with string type messages', async () => {
const message = 'Mock General connector errors';
const error = new ConnectorError(ConnectorErrorCodes.General, message);
next.mockImplementationOnce(() => {
@ -184,4 +184,23 @@ describe('koaConnectorErrorHandler middleware', () => {
)
);
});
it('General connector errors with message objects', async () => {
const message = { errorCode: 400, errorDescription: 'Mock General connector errors' };
const error = new ConnectorError(ConnectorErrorCodes.General, message);
next.mockImplementationOnce(() => {
throw error;
});
await expect(koaConnectorErrorHandler()(ctx, next)).rejects.toMatchError(
new RequestError(
{
code: 'connector.general',
status: 500,
errorDescription: '\nMock General connector errors',
},
message
)
);
});
});

View file

@ -1,5 +1,6 @@
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-types';
import { Middleware } from 'koa';
import { z } from 'zod';
import RequestError from '@/errors/RequestError';
@ -14,6 +15,10 @@ export default function koaConnectorErrorHandler<StateT, ContextT>(): Middleware
const { code, data } = error;
const errorDescriptionGuard = z.object({ errorDescription: z.string() });
const result = errorDescriptionGuard.safeParse(data);
const errorMessage = result.success ? result.data.errorDescription : undefined;
switch (code) {
case ConnectorErrorCodes.InsufficientRequestParameters:
throw new RequestError(
@ -85,6 +90,7 @@ export default function koaConnectorErrorHandler<StateT, ContextT>(): Middleware
{
code: 'connector.general',
status: 500,
errorDescription: errorMessage ? '\n' + errorMessage : undefined,
},
data
);

View file

@ -599,7 +599,7 @@ const errors = {
unsupported_prompt_name: 'Unsupported prompt name.',
},
connector: {
general: 'An unexpected error occurred in connector.',
general: 'An unexpected error occurred in connector.{{errorDescription}}',
not_found: 'Cannot find any available connector for type: {{type}}.',
not_enabled: 'The connector is not enabled.',
insufficient_request_parameters: 'The request might miss some input parameters.',

View file

@ -577,7 +577,7 @@ const errors = {
unsupported_prompt_name: '不支持的 prompt name',
},
connector: {
general: '连接器发生未知错误',
general: '连接器发生未知错误{{errorDescription}}',
not_found: '找不到可用的 {{type}} 类型的连接器',
not_enabled: '连接器尚未启用',
insufficient_request_parameters: '请求参数缺失',