diff --git a/packages/app-insights/src/node.ts b/packages/app-insights/src/node.ts index fcdd069e8..1384d33d2 100644 --- a/packages/app-insights/src/node.ts +++ b/packages/app-insights/src/node.ts @@ -4,6 +4,20 @@ import type { TelemetryClient } from 'applicationinsights'; export const normalizeError = (error: unknown) => { const normalized = error instanceof Error ? error : new Error(String(error)); + const payload = Object.entries(normalized).reduce( + (result, [key, value]) => + ['message', 'data'].includes(key) && Boolean(value) + ? // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + { ...result, [key]: value } + : result, + {} + ); + if (typeof payload === 'object' && Object.entries(payload).length > 0) { + // eslint-disable-next-line @silverhand/fp/no-mutation + normalized.message = JSON.stringify(payload); + return normalized; + } + // Add message if empty otherwise Application Insights will respond 400 // and the error will not be recorded. // eslint-disable-next-line @silverhand/fp/no-mutation diff --git a/packages/core/src/errors/RequestError/index.ts b/packages/core/src/errors/RequestError/index.ts index 8a374ff11..6ec494944 100644 --- a/packages/core/src/errors/RequestError/index.ts +++ b/packages/core/src/errors/RequestError/index.ts @@ -5,16 +5,7 @@ import { conditional, pick } from '@silverhand/essentials'; import i18next from 'i18next'; import { ZodError } from 'zod'; -const formatZodError = ({ issues }: ZodError): string[] => - issues.map((issue) => { - const base = `Error in key path "${issue.path.map(String).join('.')}": (${issue.code}) `; - - if (issue.code === 'invalid_type') { - return base + `Expected ${issue.expected} but received ${issue.received}.`; - } - - return base + issue.message; - }); +import { formatZodError } from '#src/errors/utils/index.js'; export default class RequestError extends Error { code: LogtoErrorCode; diff --git a/packages/core/src/errors/utils/index.ts b/packages/core/src/errors/utils/index.ts new file mode 100644 index 000000000..0f33c9ccc --- /dev/null +++ b/packages/core/src/errors/utils/index.ts @@ -0,0 +1,12 @@ +import type { ZodError } from 'zod'; + +export const formatZodError = ({ issues }: ZodError): string[] => + issues.map((issue) => { + const base = `Error in key path "${issue.path.map(String).join('.')}": (${issue.code}) `; + + if (issue.code === 'invalid_type') { + return base + `Expected ${issue.expected} but received ${issue.received}.`; + } + + return base + issue.message; + }); diff --git a/packages/core/src/middleware/koa-connector-error-handler.ts b/packages/core/src/middleware/koa-connector-error-handler.ts index 5bad6e65a..b2252bd40 100644 --- a/packages/core/src/middleware/koa-connector-error-handler.ts +++ b/packages/core/src/middleware/koa-connector-error-handler.ts @@ -1,9 +1,10 @@ import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit'; -import { trySafe } from '@silverhand/essentials'; +import { conditional, trySafe } from '@silverhand/essentials'; import type { Middleware } from 'koa'; import { z } from 'zod'; import RequestError from '#src/errors/RequestError/index.js'; +import { formatZodError } from '#src/errors/utils/index.js'; export default function koaConnectorErrorHandler(): Middleware { // Too many error types :-) @@ -16,7 +17,12 @@ export default function koaConnectorErrorHandler(): Middleware throw error; } - const { code, data } = error; + const { code, data, zodError, name } = error; + const requestErrorData = { + data, + name, + zodErrorMessage: conditional(zodError && formatZodError(zodError).join('\n')), + }; const errorDescriptionGuard = z.object({ errorDescription: z.string() }); const message = trySafe(() => errorDescriptionGuard.parse(data))?.errorDescription; @@ -28,14 +34,14 @@ export default function koaConnectorErrorHandler(): Middleware case ConnectorErrorCodes.InsufficientRequestParameters: case ConnectorErrorCodes.InvalidConfig: case ConnectorErrorCodes.InvalidResponse: { - throw new RequestError({ code: `connector.${code}`, status: 400 }, data); + throw new RequestError({ code: `connector.${code}`, status: 400 }, requestErrorData); } case ConnectorErrorCodes.SocialAuthCodeInvalid: case ConnectorErrorCodes.SocialAccessTokenInvalid: case ConnectorErrorCodes.SocialIdTokenInvalid: case ConnectorErrorCodes.AuthorizationFailed: { - throw new RequestError({ code: `connector.${code}`, status: 401 }, data); + throw new RequestError({ code: `connector.${code}`, status: 401 }, requestErrorData); } case ConnectorErrorCodes.TemplateNotFound: { @@ -44,16 +50,16 @@ export default function koaConnectorErrorHandler(): Middleware code: `connector.${code}`, status: 400, }, - data + requestErrorData ); } case ConnectorErrorCodes.NotImplemented: { - throw new RequestError({ code: `connector.${code}`, status: 501 }, data); + throw new RequestError({ code: `connector.${code}`, status: 501 }, requestErrorData); } case ConnectorErrorCodes.RateLimitExceeded: { - throw new RequestError({ code: `connector.${code}`, status: 429 }, data); + throw new RequestError({ code: `connector.${code}`, status: 429 }, requestErrorData); } default: { @@ -63,7 +69,7 @@ export default function koaConnectorErrorHandler(): Middleware status: 400, // Temporarily use 400 to avoid false positives. May update later. errorDescription: message, }, - data + requestErrorData ); } } diff --git a/packages/core/src/routes/authn.ts b/packages/core/src/routes/authn.ts index 6699e31a7..be1e137b9 100644 --- a/packages/core/src/routes/authn.ts +++ b/packages/core/src/routes/authn.ts @@ -114,10 +114,9 @@ export default function authnRoutes( const samlAssertionParseResult = samlAssertionGuard.safeParse(body); if (!samlAssertionParseResult.success) { - throw new ConnectorError( - ConnectorErrorCodes.InvalidResponse, - samlAssertionParseResult.error - ); + throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, { + zodError: samlAssertionParseResult.error, + }); } /** @@ -134,7 +133,7 @@ export default function authnRoutes( assertThat( validateSamlAssertion, new ConnectorError(ConnectorErrorCodes.NotImplemented, { - message: 'Method `validateSamlAssertion()` is not implemented.', + data: 'Method `validateSamlAssertion()` is not implemented.', }) ); const redirectTo = await validateSamlAssertion({ body }, getSession, setSession); diff --git a/packages/toolkit/connector-kit/src/index.ts b/packages/toolkit/connector-kit/src/index.ts index 4483ad77d..699811937 100644 --- a/packages/toolkit/connector-kit/src/index.ts +++ b/packages/toolkit/connector-kit/src/index.ts @@ -8,7 +8,7 @@ export function validateConfig(config: unknown, guard: ZodType): asserts conf const result = guard.safeParse(config); if (!result.success) { - throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, result.error); + throw new ConnectorError(ConnectorErrorCodes.InvalidConfig, { zodError: result.error }); } } @@ -20,7 +20,7 @@ export const parseJson = ( try { return JSON.parse(jsonString); } catch { - throw new ConnectorError(errorCode, errorPayload ?? jsonString); + throw new ConnectorError(errorCode, { data: errorPayload ?? jsonString }); } }; @@ -28,7 +28,7 @@ export const parseJsonObject = (...args: Parameters) => { const parsed = parseJson(...args); if (!(parsed !== null && typeof parsed === 'object')) { - throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, parsed); + throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, { data: parsed }); } return parsed; diff --git a/packages/toolkit/connector-kit/src/types.ts b/packages/toolkit/connector-kit/src/types.ts index ce225e288..bef4f198b 100644 --- a/packages/toolkit/connector-kit/src/types.ts +++ b/packages/toolkit/connector-kit/src/types.ts @@ -1,6 +1,6 @@ import type { LanguageTag } from '@logto/language-kit'; import { isLanguageTag } from '@logto/language-kit'; -import type { ZodType } from 'zod'; +import type { ZodError, ZodType } from 'zod'; import { z } from 'zod'; // MARK: Foundation @@ -59,13 +59,17 @@ export enum ConnectorErrorCodes { export class ConnectorError extends Error { public code: ConnectorErrorCodes; - public data: unknown; + public data?: unknown; + public zodError?: ZodError; - constructor(code: ConnectorErrorCodes, data?: unknown) { - const message = typeof data === 'string' ? data : 'Connector error occurred.'; + constructor(code: ConnectorErrorCodes, payload?: { data?: unknown; zodError?: ZodError }) { + const message = `Connector error occurred: ${code}`; super(message); + this.name = 'ConnectorError'; this.code = code; - this.data = typeof data === 'string' ? { message: data } : data; + const { data, zodError } = payload ?? {}; + this.data = data; + this.zodError = zodError; } }