187 lines
6.6 KiB
TypeScript
187 lines
6.6 KiB
TypeScript
export const USER_MESSAGE_KEYS = {
|
|
unknown: "messages.common.unknown",
|
|
network: "messages.network.unavailable",
|
|
badRequest: "messages.http.bad_request",
|
|
unauthorized: "messages.http.unauthorized",
|
|
forbidden: "messages.http.forbidden",
|
|
notFound: "messages.http.not_found",
|
|
conflict: "messages.http.conflict",
|
|
payloadTooLarge: "messages.http.payload_too_large",
|
|
timeout: "messages.http.timeout",
|
|
tooManyRequests: "messages.http.too_many_requests",
|
|
server: "messages.http.server",
|
|
promptRequired: "messages.validation.prompt_required",
|
|
authLoginRequired: "messages.auth.login_required",
|
|
authCodeInvalid: "messages.auth.code_invalid",
|
|
authCodeExpired: "messages.auth.code_expired",
|
|
authInvalidCredentials: "messages.auth.invalid_credentials",
|
|
authInvalidRegion: "messages.auth.invalid_region",
|
|
authProviderNotReady: "messages.auth.provider_not_ready",
|
|
authHumanVerificationRequired: "messages.auth.human_verification_required",
|
|
authHumanVerificationFailed: "messages.auth.human_verification_failed",
|
|
shareSelfInvite: "messages.share.self_invite",
|
|
shareMemberExists: "messages.share.member_exists"
|
|
} as const;
|
|
|
|
const HTTP_STATUS_MESSAGE_KEYS: Record<number, string> = {
|
|
400: USER_MESSAGE_KEYS.badRequest,
|
|
401: USER_MESSAGE_KEYS.unauthorized,
|
|
403: USER_MESSAGE_KEYS.forbidden,
|
|
404: USER_MESSAGE_KEYS.notFound,
|
|
408: USER_MESSAGE_KEYS.timeout,
|
|
409: USER_MESSAGE_KEYS.conflict,
|
|
413: USER_MESSAGE_KEYS.payloadTooLarge,
|
|
429: USER_MESSAGE_KEYS.tooManyRequests
|
|
};
|
|
|
|
const BACKEND_STATUS_MESSAGE_KEYS: Record<string, string> = {
|
|
"auth.login_required": USER_MESSAGE_KEYS.authLoginRequired,
|
|
"auth.code_invalid": USER_MESSAGE_KEYS.authCodeInvalid,
|
|
"auth.code_expired": USER_MESSAGE_KEYS.authCodeExpired,
|
|
"auth.invalid_credentials": USER_MESSAGE_KEYS.authInvalidCredentials,
|
|
"auth.invalid_region": USER_MESSAGE_KEYS.authInvalidRegion,
|
|
"auth.provider_not_ready": USER_MESSAGE_KEYS.authProviderNotReady,
|
|
"auth.human_verification_required": USER_MESSAGE_KEYS.authHumanVerificationRequired,
|
|
"auth.human_verification_failed": USER_MESSAGE_KEYS.authHumanVerificationFailed,
|
|
"share.self_invite": USER_MESSAGE_KEYS.shareSelfInvite,
|
|
"share.member_exists": USER_MESSAGE_KEYS.shareMemberExists
|
|
};
|
|
|
|
const RAW_STATUS_MESSAGE_KEYS: Array<{ pattern: RegExp; key: string }> = [
|
|
{ pattern: /verification code is invalid/i, key: USER_MESSAGE_KEYS.authCodeInvalid },
|
|
{ pattern: /verification code is expired/i, key: USER_MESSAGE_KEYS.authCodeExpired },
|
|
{ pattern: /login required/i, key: USER_MESSAGE_KEYS.authLoginRequired },
|
|
{ pattern: /prompt is required/i, key: USER_MESSAGE_KEYS.promptRequired }
|
|
];
|
|
|
|
export type BackendStatus = string | number;
|
|
|
|
export type BackendStatusBody = {
|
|
error?: string;
|
|
message?: string;
|
|
errorCode?: string;
|
|
code?: number;
|
|
requestId?: string;
|
|
traceId?: string;
|
|
};
|
|
|
|
export type RequestDomain = "auth" | "design" | "api";
|
|
|
|
export type Translate = (key: string, values?: Record<string, string | number>) => string;
|
|
|
|
export class ApiFailure extends Error {
|
|
status: number;
|
|
backendStatus: BackendStatus;
|
|
userMessageKey: string;
|
|
rawMessage: string;
|
|
body: unknown;
|
|
domain: RequestDomain;
|
|
requestId?: string;
|
|
traceId?: string;
|
|
|
|
constructor(domain: RequestDomain, status: number, statusText: string, body: unknown) {
|
|
const parsed = parseStatusBody(body);
|
|
const backendStatus = parsed.errorCode ?? parsed.code ?? status;
|
|
const userMessageKey = resolveUserMessageKey(backendStatus, status, parsed.rawMessage);
|
|
super(userMessageKey);
|
|
this.name = "ApiFailure";
|
|
this.domain = domain;
|
|
this.status = status;
|
|
this.backendStatus = backendStatus;
|
|
this.userMessageKey = userMessageKey;
|
|
this.rawMessage = parsed.rawMessage || statusText;
|
|
this.body = body;
|
|
this.requestId = parsed.requestId;
|
|
this.traceId = parsed.traceId;
|
|
}
|
|
}
|
|
|
|
export class NetworkFailure extends Error {
|
|
backendStatus = "network.unavailable";
|
|
userMessageKey = USER_MESSAGE_KEYS.network;
|
|
domain: RequestDomain;
|
|
cause?: unknown;
|
|
|
|
constructor(domain: RequestDomain, cause?: unknown) {
|
|
super(USER_MESSAGE_KEYS.network);
|
|
this.name = "NetworkFailure";
|
|
this.domain = domain;
|
|
this.cause = cause;
|
|
}
|
|
}
|
|
|
|
export function createApiFailure(domain: RequestDomain, response: Response, body: unknown) {
|
|
return new ApiFailure(domain, response.status, response.statusText, body);
|
|
}
|
|
|
|
export function createNetworkFailure(domain: RequestDomain, cause?: unknown) {
|
|
return new NetworkFailure(domain, cause);
|
|
}
|
|
|
|
export function backendStatusOf(value: unknown): BackendStatus | undefined {
|
|
if (value instanceof ApiFailure || value instanceof NetworkFailure) {
|
|
return value.backendStatus;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function userMessageKeyOf(value: unknown) {
|
|
if (value instanceof ApiFailure || value instanceof NetworkFailure) {
|
|
return value.userMessageKey;
|
|
}
|
|
return USER_MESSAGE_KEYS.unknown;
|
|
}
|
|
|
|
export function httpStatusOf(value: unknown) {
|
|
return value instanceof ApiFailure ? value.status : undefined;
|
|
}
|
|
|
|
export function isBackendStatus(value: unknown, status: BackendStatus) {
|
|
return backendStatusOf(value) === status;
|
|
}
|
|
|
|
export function isUserMessageKey(value: unknown, key: string) {
|
|
return userMessageKeyOf(value) === key;
|
|
}
|
|
|
|
export function toUserMessage(value: unknown, t: Translate, fallbackKey: string = USER_MESSAGE_KEYS.unknown) {
|
|
const key = userMessageKeyOf(value);
|
|
const message = t(key);
|
|
if (message !== key) return message;
|
|
return t(fallbackKey);
|
|
}
|
|
|
|
function parseStatusBody(body: unknown) {
|
|
if (!body || typeof body !== "object") {
|
|
return {};
|
|
}
|
|
const data = body as BackendStatusBody;
|
|
return {
|
|
rawMessage: stringValue(data.message) || stringValue(data.error),
|
|
errorCode: stringValue(data.errorCode),
|
|
code: typeof data.code === "number" ? data.code : undefined,
|
|
requestId: stringValue(data.requestId),
|
|
traceId: stringValue(data.traceId)
|
|
};
|
|
}
|
|
|
|
function resolveUserMessageKey(backendStatus: BackendStatus, httpStatus: number, rawMessage: string | undefined) {
|
|
if (typeof backendStatus === "string" && BACKEND_STATUS_MESSAGE_KEYS[backendStatus]) {
|
|
return BACKEND_STATUS_MESSAGE_KEYS[backendStatus];
|
|
}
|
|
for (const item of RAW_STATUS_MESSAGE_KEYS) {
|
|
if (item.pattern.test(rawMessage ?? "")) return item.key;
|
|
}
|
|
if (typeof backendStatus === "number" && HTTP_STATUS_MESSAGE_KEYS[backendStatus]) {
|
|
return HTTP_STATUS_MESSAGE_KEYS[backendStatus];
|
|
}
|
|
if (HTTP_STATUS_MESSAGE_KEYS[httpStatus]) {
|
|
return HTTP_STATUS_MESSAGE_KEYS[httpStatus];
|
|
}
|
|
return httpStatus >= 500 ? USER_MESSAGE_KEYS.server : USER_MESSAGE_KEYS.unknown;
|
|
}
|
|
|
|
function stringValue(value: unknown) {
|
|
return typeof value === "string" && value.trim() ? value : undefined;
|
|
}
|