0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

fix: azure with detailed error messages

This commit is contained in:
Darcy Ye 2023-06-03 12:19:30 +08:00
parent fb2bb527aa
commit defddf7e08
No known key found for this signature in database
GPG key ID: B46F4C07EDEFC610
7 changed files with 57 additions and 31 deletions

View file

@ -4,6 +4,20 @@ import type { TelemetryClient } from 'applicationinsights';
export const normalizeError = (error: unknown) => { export const normalizeError = (error: unknown) => {
const normalized = error instanceof Error ? error : new Error(String(error)); 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 // Add message if empty otherwise Application Insights will respond 400
// and the error will not be recorded. // and the error will not be recorded.
// eslint-disable-next-line @silverhand/fp/no-mutation // eslint-disable-next-line @silverhand/fp/no-mutation

View file

@ -5,16 +5,7 @@ import { conditional, pick } from '@silverhand/essentials';
import i18next from 'i18next'; import i18next from 'i18next';
import { ZodError } from 'zod'; import { ZodError } from 'zod';
const formatZodError = ({ issues }: ZodError): string[] => import { formatZodError } from '#src/errors/utils/index.js';
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;
});
export default class RequestError extends Error { export default class RequestError extends Error {
code: LogtoErrorCode; code: LogtoErrorCode;

View file

@ -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;
});

View file

@ -1,9 +1,10 @@
import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit'; import { ConnectorError, ConnectorErrorCodes } from '@logto/connector-kit';
import { trySafe } from '@silverhand/essentials'; import { conditional, trySafe } from '@silverhand/essentials';
import type { Middleware } from 'koa'; import type { Middleware } from 'koa';
import { z } from 'zod'; import { z } from 'zod';
import RequestError from '#src/errors/RequestError/index.js'; import RequestError from '#src/errors/RequestError/index.js';
import { formatZodError } from '#src/errors/utils/index.js';
export default function koaConnectorErrorHandler<StateT, ContextT>(): Middleware<StateT, ContextT> { export default function koaConnectorErrorHandler<StateT, ContextT>(): Middleware<StateT, ContextT> {
// Too many error types :-) // Too many error types :-)
@ -16,7 +17,12 @@ export default function koaConnectorErrorHandler<StateT, ContextT>(): Middleware
throw error; 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 errorDescriptionGuard = z.object({ errorDescription: z.string() });
const message = trySafe(() => errorDescriptionGuard.parse(data))?.errorDescription; const message = trySafe(() => errorDescriptionGuard.parse(data))?.errorDescription;
@ -28,14 +34,14 @@ export default function koaConnectorErrorHandler<StateT, ContextT>(): Middleware
case ConnectorErrorCodes.InsufficientRequestParameters: case ConnectorErrorCodes.InsufficientRequestParameters:
case ConnectorErrorCodes.InvalidConfig: case ConnectorErrorCodes.InvalidConfig:
case ConnectorErrorCodes.InvalidResponse: { 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.SocialAuthCodeInvalid:
case ConnectorErrorCodes.SocialAccessTokenInvalid: case ConnectorErrorCodes.SocialAccessTokenInvalid:
case ConnectorErrorCodes.SocialIdTokenInvalid: case ConnectorErrorCodes.SocialIdTokenInvalid:
case ConnectorErrorCodes.AuthorizationFailed: { case ConnectorErrorCodes.AuthorizationFailed: {
throw new RequestError({ code: `connector.${code}`, status: 401 }, data); throw new RequestError({ code: `connector.${code}`, status: 401 }, requestErrorData);
} }
case ConnectorErrorCodes.TemplateNotFound: { case ConnectorErrorCodes.TemplateNotFound: {
@ -44,16 +50,16 @@ export default function koaConnectorErrorHandler<StateT, ContextT>(): Middleware
code: `connector.${code}`, code: `connector.${code}`,
status: 400, status: 400,
}, },
data requestErrorData
); );
} }
case ConnectorErrorCodes.NotImplemented: { case ConnectorErrorCodes.NotImplemented: {
throw new RequestError({ code: `connector.${code}`, status: 501 }, data); throw new RequestError({ code: `connector.${code}`, status: 501 }, requestErrorData);
} }
case ConnectorErrorCodes.RateLimitExceeded: { case ConnectorErrorCodes.RateLimitExceeded: {
throw new RequestError({ code: `connector.${code}`, status: 429 }, data); throw new RequestError({ code: `connector.${code}`, status: 429 }, requestErrorData);
} }
default: { default: {
@ -63,7 +69,7 @@ export default function koaConnectorErrorHandler<StateT, ContextT>(): Middleware
status: 400, // Temporarily use 400 to avoid false positives. May update later. status: 400, // Temporarily use 400 to avoid false positives. May update later.
errorDescription: message, errorDescription: message,
}, },
data requestErrorData
); );
} }
} }

View file

@ -114,10 +114,9 @@ export default function authnRoutes<T extends AnonymousRouter>(
const samlAssertionParseResult = samlAssertionGuard.safeParse(body); const samlAssertionParseResult = samlAssertionGuard.safeParse(body);
if (!samlAssertionParseResult.success) { if (!samlAssertionParseResult.success) {
throw new ConnectorError( throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, {
ConnectorErrorCodes.InvalidResponse, zodError: samlAssertionParseResult.error,
samlAssertionParseResult.error });
);
} }
/** /**
@ -134,7 +133,7 @@ export default function authnRoutes<T extends AnonymousRouter>(
assertThat( assertThat(
validateSamlAssertion, validateSamlAssertion,
new ConnectorError(ConnectorErrorCodes.NotImplemented, { new ConnectorError(ConnectorErrorCodes.NotImplemented, {
message: 'Method `validateSamlAssertion()` is not implemented.', data: 'Method `validateSamlAssertion()` is not implemented.',
}) })
); );
const redirectTo = await validateSamlAssertion({ body }, getSession, setSession); const redirectTo = await validateSamlAssertion({ body }, getSession, setSession);

View file

@ -8,7 +8,7 @@ export function validateConfig<T>(config: unknown, guard: ZodType): asserts conf
const result = guard.safeParse(config); const result = guard.safeParse(config);
if (!result.success) { 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 { try {
return JSON.parse(jsonString); return JSON.parse(jsonString);
} catch { } catch {
throw new ConnectorError(errorCode, errorPayload ?? jsonString); throw new ConnectorError(errorCode, { data: errorPayload ?? jsonString });
} }
}; };
@ -28,7 +28,7 @@ export const parseJsonObject = (...args: Parameters<typeof parseJson>) => {
const parsed = parseJson(...args); const parsed = parseJson(...args);
if (!(parsed !== null && typeof parsed === 'object')) { if (!(parsed !== null && typeof parsed === 'object')) {
throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, parsed); throw new ConnectorError(ConnectorErrorCodes.InvalidResponse, { data: parsed });
} }
return parsed; return parsed;

View file

@ -1,6 +1,6 @@
import type { LanguageTag } from '@logto/language-kit'; import type { LanguageTag } from '@logto/language-kit';
import { isLanguageTag } 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'; import { z } from 'zod';
// MARK: Foundation // MARK: Foundation
@ -59,13 +59,17 @@ export enum ConnectorErrorCodes {
export class ConnectorError extends Error { export class ConnectorError extends Error {
public code: ConnectorErrorCodes; public code: ConnectorErrorCodes;
public data: unknown; public data?: unknown;
public zodError?: ZodError;
constructor(code: ConnectorErrorCodes, data?: unknown) { constructor(code: ConnectorErrorCodes, payload?: { data?: unknown; zodError?: ZodError }) {
const message = typeof data === 'string' ? data : 'Connector error occurred.'; const message = `Connector error occurred: ${code}`;
super(message); super(message);
this.name = 'ConnectorError';
this.code = code; this.code = code;
this.data = typeof data === 'string' ? { message: data } : data; const { data, zodError } = payload ?? {};
this.data = data;
this.zodError = zodError;
} }
} }