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); const result = appleConfigGuard.safeParse(config);
if (!result.success) { 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 { export class ConnectorError extends Error {
public code: ConnectorErrorCodes; 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); super(message);
this.code = code; this.code = code;
this.data = typeof data === 'string' ? { message: data } : data;
} }
} }

View file

@ -31,5 +31,6 @@ div.toast {
&.error { &.error {
border: 1px solid var(--color-error); border: 1px solid var(--color-error);
background-color: var(--color-danger-toast-background); 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) => { const toastError = async (response: Response) => {
try { try {
const data = await response.json<RequestErrorBody>(); 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 { } catch {
toast.error(t('admin_console.errors.unknown_server_error')); toast.error(t('admin_console.errors.unknown_server_error'));
} }

View file

@ -1,8 +1,22 @@
import { LogtoErrorCode, LogtoErrorI18nKey } from '@logto/phrases'; import { LogtoErrorCode, LogtoErrorI18nKey } from '@logto/phrases';
import { RequestErrorBody, RequestErrorMetadata } from '@logto/schemas'; import { RequestErrorBody, RequestErrorMetadata } from '@logto/schemas';
import { conditional, Optional } from '@silverhand/essentials';
import i18next from 'i18next'; import i18next from 'i18next';
import pick from 'lodash.pick'; 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 { export default class RequestError extends Error {
code: LogtoErrorCode; code: LogtoErrorCode;
status: number; status: number;
@ -27,6 +41,10 @@ export default class RequestError extends Error {
} }
get body(): RequestErrorBody { 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; throw error;
} }
const { code, message } = error; const { code, data } = error;
// Original OIDCProvider Error description and details are provided in the data field
const data = { message };
switch (code) { switch (code) {
case ConnectorErrorCodes.InsufficientRequestParameters: 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 { MiddlewareType } from 'koa';
import koaBody from 'koa-body'; import koaBody from 'koa-body';
import { IMiddleware, IRouterParamContext } from 'koa-router'; import { IMiddleware, IRouterParamContext } from 'koa-router';
@ -41,6 +41,18 @@ export const isGuardMiddleware = <Type extends IMiddleware>(
): function_ is WithGuardConfig<Type> => ): function_ is WithGuardConfig<Type> =>
function_.name === 'guardMiddleware' && has(function_, 'config'); 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< export default function koaGuard<
StateT, StateT,
ContextT extends IRouterParamContext, ContextT extends IRouterParamContext,
@ -62,16 +74,12 @@ export default function koaGuard<
WithGuardedContext<ContextT, GuardQueryT, GuardBodyT, GuardParametersT>, WithGuardedContext<ContextT, GuardQueryT, GuardBodyT, GuardParametersT>,
ResponseBodyT ResponseBodyT
> = async (ctx, next) => { > = async (ctx, next) => {
try { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions ctx.guard = {
ctx.guard = { query: tryParse('query', query, ctx.request.query),
query: query?.parse(ctx.request.query), body: tryParse('body', body, ctx.request.body),
body: body?.parse(ctx.request.body), params: tryParse('params', params, ctx.params),
params: params?.parse(ctx.params), } as Guarded<GuardQueryT, GuardBodyT, GuardParametersT>; // Have to do this since it's too complicated for TS
} 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);
}
return next(); return next();
}; };

View file

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

View file

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

View file

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