0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-17 22:04:19 -05:00

feat: expose zod error (#1474)

This commit is contained in:
Gao Sun 2022-07-08 16:05:43 +08:00 committed by GitHub
parent bb790ce4d1
commit 81b63f07bb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 56 additions and 22 deletions

View file

@ -23,7 +23,7 @@ export default class AppleConnector implements SocialConnector {
const result = appleConfigGuard.safeParse(config);
if (!result.success) {
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error.message);
throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error);
}
};

View file

@ -41,10 +41,13 @@ export enum ConnectorErrorCodes {
export class ConnectorError extends Error {
public code: ConnectorErrorCodes;
public data: unknown;
constructor(code: ConnectorErrorCodes, message?: string) {
constructor(code: ConnectorErrorCodes, data?: unknown) {
const message = typeof data === 'string' ? data : 'Connector error occurred.';
super(message);
this.code = code;
this.data = typeof data === 'string' ? { message: data } : data;
}
}

View file

@ -31,5 +31,6 @@ div.toast {
&.error {
border: 1px solid var(--color-error);
background-color: var(--color-danger-toast-background);
white-space: pre-line;
}
}

View file

@ -20,7 +20,9 @@ export class RequestError extends Error {
const toastError = async (response: Response) => {
try {
const data = await response.json<RequestErrorBody>();
toast.error(data.message || t('admin_console.errors.unknown_server_error'));
toast.error(
[data.message, data.details].join('\n') || t('admin_console.errors.unknown_server_error')
);
} catch {
toast.error(t('admin_console.errors.unknown_server_error'));
}

View file

@ -1,8 +1,22 @@
import { LogtoErrorCode, LogtoErrorI18nKey } from '@logto/phrases';
import { RequestErrorBody, RequestErrorMetadata } from '@logto/schemas';
import { conditional, Optional } from '@silverhand/essentials';
import i18next from 'i18next';
import pick from 'lodash.pick';
import { ZodError } from 'zod';
const formatZodError = ({ issues }: ZodError): string[] =>
issues.map((issue) => {
const base = `Error in key path "${issue.path.map((node) => String(node)).join('.')}": (${
issue.code
}) `;
if (issue.code === 'invalid_type') {
return base + `Expected ${issue.expected} but received ${issue.received}.`;
}
return base + issue.message;
});
export default class RequestError extends Error {
code: LogtoErrorCode;
status: number;
@ -27,6 +41,10 @@ export default class RequestError extends Error {
}
get body(): RequestErrorBody {
return pick(this, 'message', 'code', 'data');
return pick(this, 'message', 'code', 'data', 'details');
}
get details(): Optional<string> {
return conditional(this.data instanceof ZodError && formatZodError(this.data).join('\n'));
}
}

View file

@ -12,10 +12,7 @@ export default function koaConnectorErrorHandler<StateT, ContextT>(): Middleware
throw error;
}
const { code, message } = error;
// Original OIDCProvider Error description and details are provided in the data field
const data = { message };
const { code, data } = error;
switch (code) {
case ConnectorErrorCodes.InsufficientRequestParameters:

View file

@ -1,4 +1,4 @@
import { has } from '@silverhand/essentials';
import { has, Optional } from '@silverhand/essentials';
import { MiddlewareType } from 'koa';
import koaBody from 'koa-body';
import { IMiddleware, IRouterParamContext } from 'koa-router';
@ -41,6 +41,18 @@ export const isGuardMiddleware = <Type extends IMiddleware>(
): function_ is WithGuardConfig<Type> =>
function_.name === 'guardMiddleware' && has(function_, 'config');
const tryParse = <Output, Definition, Input>(
type: 'query' | 'body' | 'params',
guard: Optional<ZodType<Output, Definition, Input>>,
data: unknown
) => {
try {
return guard?.parse(data);
} catch (error: unknown) {
throw new RequestError({ code: 'guard.invalid_input', type }, error);
}
};
export default function koaGuard<
StateT,
ContextT extends IRouterParamContext,
@ -62,16 +74,12 @@ export default function koaGuard<
WithGuardedContext<ContextT, GuardQueryT, GuardBodyT, GuardParametersT>,
ResponseBodyT
> = async (ctx, next) => {
try {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
ctx.guard = {
query: query?.parse(ctx.request.query),
body: body?.parse(ctx.request.body),
params: params?.parse(ctx.params),
} as Guarded<GuardQueryT, GuardBodyT, GuardParametersT>; // Have to do this since it's too complicated for TS
} catch (error: unknown) {
throw new RequestError('guard.invalid_input', error);
}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
ctx.guard = {
query: tryParse('query', query, ctx.request.query),
body: tryParse('body', body, ctx.request.body),
params: tryParse('params', params, ctx.params),
} as Guarded<GuardQueryT, GuardBodyT, GuardParametersT>; // Have to do this since it's too complicated for TS
return next();
};

View file

@ -546,7 +546,7 @@ const errors = {
jwt_sub_missing: 'Missing `sub` in JWT.',
},
guard: {
invalid_input: 'The request input is invalid.',
invalid_input: 'The request {{type}} is invalid.',
invalid_pagination: 'The request pagination value is invalid.',
},
oidc: {

View file

@ -524,7 +524,7 @@ const errors = {
jwt_sub_missing: 'JWT 缺失 `sub`',
},
guard: {
invalid_input: '请求输入无效',
invalid_input: '请求中 {{type}} 无效',
invalid_pagination: '分页参数无效',
},
oidc: {

View file

@ -6,4 +6,9 @@ export type RequestErrorMetadata = Record<string, unknown> & {
expose?: boolean;
};
export type RequestErrorBody = { message: string; data: unknown; code: LogtoErrorCode };
export type RequestErrorBody = {
message: string;
data: unknown;
code: LogtoErrorCode;
details?: string;
};