Implement DomainHttpException for stable error handling and refactor exception filter
- Introduced `DomainHttpException` to standardize HTTP error responses with explicit domain error codes. - Refactored `UnifiedExceptionFilter` to extract detailed exception information, including optional explicit error codes. - Updated `WhmcsErrorHandlerService` to utilize `DomainHttpException` for better error normalization and handling. - Removed deprecated error pattern matching logic to enforce explicit error codes in the system. - Enhanced logging in `WhmcsHttpClientService` to differentiate between expected business outcomes and actual errors.
This commit is contained in:
parent
c13a327a07
commit
ad6fadb015
21
apps/bff/src/core/http/domain-http.exception.ts
Normal file
21
apps/bff/src/core/http/domain-http.exception.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { HttpException } from "@nestjs/common";
|
||||
import type { HttpStatus } from "@nestjs/common";
|
||||
import { ErrorMessages, type ErrorCodeType } from "@customer-portal/domain/common";
|
||||
|
||||
/**
|
||||
* DomainHttpException
|
||||
*
|
||||
* Use this when you want to throw an HTTP error with an explicit, stable domain error code.
|
||||
* The global exception filter will read `code` from the exception response and format the
|
||||
* standard API error response for the client.
|
||||
*/
|
||||
export class DomainHttpException extends HttpException {
|
||||
constructor(
|
||||
public readonly code: ErrorCodeType,
|
||||
status: HttpStatus,
|
||||
message?: string
|
||||
) {
|
||||
// `message` is optional; if omitted we fall back to the shared user-facing message.
|
||||
super({ code, message: message ?? ErrorMessages[code] }, status);
|
||||
}
|
||||
}
|
||||
@ -14,7 +14,6 @@ import {
|
||||
ErrorCode,
|
||||
ErrorMessages,
|
||||
ErrorMetadata,
|
||||
matchErrorPattern,
|
||||
type ErrorCodeType,
|
||||
type ApiError,
|
||||
} from "@customer-portal/domain/common";
|
||||
@ -78,65 +77,92 @@ export class UnifiedExceptionFilter implements ExceptionFilter {
|
||||
} {
|
||||
let status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
let originalMessage = "An unexpected error occurred";
|
||||
let explicitCode: ErrorCodeType | undefined;
|
||||
|
||||
if (exception instanceof HttpException) {
|
||||
status = exception.getStatus();
|
||||
originalMessage = this.extractExceptionMessage(exception);
|
||||
const extracted = this.extractExceptionDetails(exception);
|
||||
originalMessage = extracted.message;
|
||||
explicitCode = extracted.code;
|
||||
} else if (exception instanceof Error) {
|
||||
originalMessage = exception.message;
|
||||
}
|
||||
|
||||
// Map to error code
|
||||
const errorCode = this.mapToErrorCode(originalMessage, status);
|
||||
const errorCode = explicitCode ?? this.mapStatusToErrorCode(status);
|
||||
|
||||
return { status, errorCode, originalMessage };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract message from HttpException
|
||||
* Extract message (and optionally an explicit error code) from HttpException.
|
||||
*/
|
||||
private extractExceptionMessage(exception: HttpException): string {
|
||||
private extractExceptionDetails(exception: HttpException): {
|
||||
message: string;
|
||||
code?: ErrorCodeType;
|
||||
} {
|
||||
const response = exception.getResponse();
|
||||
|
||||
if (typeof response === "string") {
|
||||
return response;
|
||||
return { message: response };
|
||||
}
|
||||
|
||||
if (typeof response === "object" && response !== null) {
|
||||
const responseObj = response as Record<string, unknown>;
|
||||
const code = this.extractExplicitCode(responseObj);
|
||||
|
||||
// Handle NestJS validation errors (array of messages)
|
||||
if (Array.isArray(responseObj.message)) {
|
||||
const firstMessage = responseObj.message.find((m): m is string => typeof m === "string");
|
||||
if (firstMessage) return firstMessage;
|
||||
if (firstMessage) return { message: firstMessage, code };
|
||||
}
|
||||
|
||||
// Handle standard message field
|
||||
if (typeof responseObj.message === "string") {
|
||||
return responseObj.message;
|
||||
return { message: responseObj.message, code };
|
||||
}
|
||||
|
||||
// Handle error field
|
||||
if (typeof responseObj.error === "string") {
|
||||
return responseObj.error;
|
||||
return { message: responseObj.error, code };
|
||||
}
|
||||
|
||||
return { message: exception.message, code };
|
||||
}
|
||||
|
||||
return exception.message;
|
||||
return { message: exception.message };
|
||||
}
|
||||
|
||||
/**
|
||||
* Map error message and status to error code
|
||||
* Extract explicit error code from HttpException response bodies.
|
||||
*/
|
||||
private mapToErrorCode(message: string, status: number): ErrorCodeType {
|
||||
// First, try pattern matching on the message
|
||||
const patternCode = matchErrorPattern(message);
|
||||
if (patternCode !== ErrorCode.UNKNOWN) {
|
||||
return patternCode;
|
||||
private extractExplicitCode(responseObj: Record<string, unknown>): ErrorCodeType | undefined {
|
||||
// 1) Preferred: { code: "AUTH_003" }
|
||||
if (typeof responseObj.code === "string" && this.isKnownErrorCode(responseObj.code)) {
|
||||
return responseObj.code as ErrorCodeType;
|
||||
}
|
||||
|
||||
// Fall back to status code mapping
|
||||
// Cast status to HttpStatus to satisfy TypeScript enum comparison
|
||||
// 2) Standard API error format: { error: { code: "AUTH_003" } }
|
||||
const maybeError = responseObj.error;
|
||||
if (maybeError && typeof maybeError === "object") {
|
||||
const errorObj = maybeError as Record<string, unknown>;
|
||||
if (typeof errorObj.code === "string" && this.isKnownErrorCode(errorObj.code)) {
|
||||
return errorObj.code as ErrorCodeType;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private isKnownErrorCode(value: string): boolean {
|
||||
// ErrorMessages is keyed by ErrorCodeType
|
||||
return Object.prototype.hasOwnProperty.call(ErrorMessages, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map status code to error code (no message parsing).
|
||||
*/
|
||||
private mapStatusToErrorCode(status: number): ErrorCodeType {
|
||||
switch (status as HttpStatus) {
|
||||
case HttpStatus.UNAUTHORIZED:
|
||||
return ErrorCode.SESSION_EXPIRED;
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { Injectable, HttpStatus } from "@nestjs/common";
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
UnauthorizedException,
|
||||
} from "@nestjs/common";
|
||||
ErrorCode,
|
||||
type ErrorCodeType,
|
||||
type WhmcsErrorResponse,
|
||||
} from "@customer-portal/domain/common";
|
||||
import { DomainHttpException } from "@bff/core/http/domain-http.exception.js";
|
||||
import { getErrorMessage } from "@bff/core/utils/error.util.js";
|
||||
import type { WhmcsErrorResponse } from "@customer-portal/domain/common";
|
||||
|
||||
/**
|
||||
* Service for handling and normalizing WHMCS API errors
|
||||
@ -24,69 +24,72 @@ export class WhmcsErrorHandlerService {
|
||||
const message = errorResponse.message;
|
||||
const errorCode = errorResponse.errorcode;
|
||||
|
||||
// Normalize common, expected error responses to domain exceptions
|
||||
if (this.isNotFoundError(action, message)) {
|
||||
throw this.createNotFoundException(action, message, params);
|
||||
}
|
||||
|
||||
if (this.isAuthenticationError(message, errorCode)) {
|
||||
throw new UnauthorizedException(`WHMCS Authentication Error: ${message}`);
|
||||
}
|
||||
|
||||
if (this.isValidationError(message, errorCode)) {
|
||||
throw new BadRequestException(`WHMCS Validation Error: ${message}`);
|
||||
}
|
||||
|
||||
// Generic WHMCS API error
|
||||
throw new Error(`WHMCS API Error [${action}]: ${message} (${errorCode || "unknown"})`);
|
||||
const mapped = this.mapProviderErrorToDomain(action, message, errorCode, params);
|
||||
throw new DomainHttpException(mapped.code, mapped.status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle general request errors (network, timeout, etc.)
|
||||
*/
|
||||
handleRequestError(error: unknown, action: string, _params: Record<string, unknown>): never {
|
||||
const message = getErrorMessage(error);
|
||||
|
||||
handleRequestError(error: unknown, _action: string, _params: Record<string, unknown>): never {
|
||||
if (this.isTimeoutError(error)) {
|
||||
throw new Error(`WHMCS API timeout [${action}]: Request timed out`);
|
||||
throw new DomainHttpException(ErrorCode.TIMEOUT, HttpStatus.GATEWAY_TIMEOUT);
|
||||
}
|
||||
|
||||
if (this.isNetworkError(error)) {
|
||||
throw new Error(`WHMCS API network error [${action}]: ${message}`);
|
||||
throw new DomainHttpException(ErrorCode.NETWORK_ERROR, HttpStatus.BAD_GATEWAY);
|
||||
}
|
||||
|
||||
// Re-throw the original error if it's already a known exception type
|
||||
if (this.isKnownException(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Generic request error
|
||||
throw new Error(`WHMCS API request failed [${action}]: ${message}`);
|
||||
// If upstream already threw a DomainHttpException or HttpException with code,
|
||||
// let the global exception filter handle it.
|
||||
throw error;
|
||||
}
|
||||
|
||||
private mapProviderErrorToDomain(
|
||||
action: string,
|
||||
message: string,
|
||||
providerErrorCode: string | undefined,
|
||||
params: Record<string, unknown>
|
||||
): { code: ErrorCodeType; status: HttpStatus } {
|
||||
// 1) ValidateLogin: user credentials are wrong (expected)
|
||||
if (
|
||||
action === "ValidateLogin" &&
|
||||
this.isValidateLoginInvalidCredentials(message, providerErrorCode)
|
||||
) {
|
||||
return { code: ErrorCode.INVALID_CREDENTIALS, status: HttpStatus.UNAUTHORIZED };
|
||||
}
|
||||
|
||||
// 2) Not-found style outcomes (expected for some reads)
|
||||
if (this.isNotFoundError(action, message)) {
|
||||
return { code: ErrorCode.NOT_FOUND, status: HttpStatus.NOT_FOUND };
|
||||
}
|
||||
|
||||
// 3) WHMCS API key auth failures: external service/config problem (not end-user auth)
|
||||
if (this.isAuthenticationError(message, providerErrorCode)) {
|
||||
return { code: ErrorCode.EXTERNAL_SERVICE_ERROR, status: HttpStatus.SERVICE_UNAVAILABLE };
|
||||
}
|
||||
|
||||
// 4) Validation failures: treat as bad request
|
||||
if (this.isValidationError(message, providerErrorCode)) {
|
||||
return { code: ErrorCode.VALIDATION_FAILED, status: HttpStatus.BAD_REQUEST };
|
||||
}
|
||||
|
||||
// 5) Default: external service error
|
||||
void params; // reserved for future mapping detail; keep signature stable
|
||||
return { code: ErrorCode.EXTERNAL_SERVICE_ERROR, status: HttpStatus.BAD_GATEWAY };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error indicates a not found condition
|
||||
*/
|
||||
private isNotFoundError(action: string, message: string): boolean {
|
||||
const lowerMessage = message.toLowerCase();
|
||||
|
||||
// Client not found errors
|
||||
if (action === "GetClientsDetails" && lowerMessage.includes("client not found")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Invoice not found errors
|
||||
if (action === "GetClientsDetails" && lowerMessage.includes("client not found")) return true;
|
||||
if (
|
||||
(action === "GetInvoice" || action === "UpdateInvoice") &&
|
||||
lowerMessage.includes("invoice not found")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Product not found errors
|
||||
if (action === "GetClientsProducts" && lowerMessage.includes("no products found")) {
|
||||
return true;
|
||||
}
|
||||
if (action === "GetClientsProducts" && lowerMessage.includes("no products found")) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
@ -123,40 +126,19 @@ export class WhmcsErrorHandlerService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create appropriate NotFoundException based on action and params
|
||||
* ValidateLogin returns user-facing "invalid credentials" strings (e.g. "Email or Password Invalid").
|
||||
* Treat these as authentication failures rather than external service errors.
|
||||
*/
|
||||
private createNotFoundException(
|
||||
action: string,
|
||||
message: string,
|
||||
params: Record<string, unknown>
|
||||
): NotFoundException {
|
||||
if (action === "GetClientsDetails") {
|
||||
const emailParam = params["email"];
|
||||
if (typeof emailParam === "string") {
|
||||
return new NotFoundException(`Client with email ${emailParam} not found`);
|
||||
}
|
||||
private isValidateLoginInvalidCredentials(message: string, errorCode?: string): boolean {
|
||||
const lowerMessage = message.toLowerCase();
|
||||
|
||||
const clientIdParam = params["clientid"];
|
||||
const identifier =
|
||||
typeof clientIdParam === "string" || typeof clientIdParam === "number"
|
||||
? clientIdParam
|
||||
: "unknown";
|
||||
// WHMCS commonly responds with: "Email or Password Invalid"
|
||||
if (lowerMessage.includes("email or password invalid")) return true;
|
||||
if (lowerMessage.includes("password invalid")) return true;
|
||||
if (lowerMessage.includes("invalid login")) return true;
|
||||
if (errorCode === "EMAIL_OR_PASSWORD_INVALID") return true;
|
||||
|
||||
return new NotFoundException(`Client with ID ${identifier} not found`);
|
||||
}
|
||||
|
||||
if (action === "GetInvoice" || action === "UpdateInvoice") {
|
||||
const invoiceIdParam = params["invoiceid"];
|
||||
const identifier =
|
||||
typeof invoiceIdParam === "string" || typeof invoiceIdParam === "number"
|
||||
? invoiceIdParam
|
||||
: "unknown";
|
||||
|
||||
return new NotFoundException(`Invoice with ID ${identifier} not found`);
|
||||
}
|
||||
|
||||
// Generic not found error
|
||||
return new NotFoundException(message);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -185,33 +167,10 @@ export class WhmcsErrorHandlerService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if error is already a known NestJS exception
|
||||
*/
|
||||
private isKnownException(error: unknown): boolean {
|
||||
return (
|
||||
error instanceof NotFoundException ||
|
||||
error instanceof BadRequestException ||
|
||||
error instanceof UnauthorizedException
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user-friendly error message for client consumption
|
||||
*/
|
||||
getUserFriendlyMessage(error: unknown): string {
|
||||
if (error instanceof NotFoundException) {
|
||||
return "The requested resource was not found.";
|
||||
}
|
||||
|
||||
if (error instanceof BadRequestException) {
|
||||
return "The request contains invalid data.";
|
||||
}
|
||||
|
||||
if (error instanceof UnauthorizedException) {
|
||||
return "Authentication failed. Please check your credentials.";
|
||||
}
|
||||
|
||||
const message = getErrorMessage(error).toLowerCase();
|
||||
|
||||
if (message.includes("timeout")) {
|
||||
|
||||
@ -274,11 +274,13 @@ export class WhmcsHttpClientService {
|
||||
const errorMessage = this.toDisplayString(message ?? error, "Unknown WHMCS API error");
|
||||
const errorCode = this.toDisplayString(errorcode, "unknown");
|
||||
|
||||
this.logger.error(`WHMCS API returned error [${action}]`, {
|
||||
// Many WHMCS "result=error" responses are expected business outcomes (e.g. invalid credentials).
|
||||
// Log as warning (not error) to avoid spamming error logs; the unified exception filter will
|
||||
// still emit the request-level log based on the mapped error code.
|
||||
this.logger.warn(`WHMCS API returned error [${action}]`, {
|
||||
errorMessage,
|
||||
errorCode,
|
||||
params: this.sanitizeLogParams(params),
|
||||
fullResponse: parsedResponse,
|
||||
});
|
||||
|
||||
// Return error response for the orchestrator to handle with proper exception types
|
||||
|
||||
@ -10,7 +10,6 @@ import {
|
||||
ErrorCode,
|
||||
ErrorMessages,
|
||||
ErrorMetadata,
|
||||
matchErrorPattern,
|
||||
type ErrorCodeType,
|
||||
} from "@customer-portal/domain/common";
|
||||
|
||||
@ -46,13 +45,11 @@ export function parseError(error: unknown): ParsedError {
|
||||
|
||||
// Handle string errors
|
||||
if (typeof error === "string") {
|
||||
const code = matchErrorPattern(error);
|
||||
const metadata = ErrorMetadata[code];
|
||||
return {
|
||||
code,
|
||||
code: ErrorCode.UNKNOWN,
|
||||
message: error,
|
||||
shouldLogout: metadata.shouldLogout,
|
||||
shouldRetry: metadata.shouldRetry,
|
||||
shouldLogout: false,
|
||||
shouldRetry: true,
|
||||
};
|
||||
}
|
||||
|
||||
@ -77,11 +74,7 @@ function parseApiError(error: ClientApiError): ParsedError {
|
||||
const bodyObj = body as Record<string, unknown>;
|
||||
|
||||
// Check for standard { success: false, error: { code, message } } format
|
||||
if (
|
||||
bodyObj.success === false &&
|
||||
bodyObj.error &&
|
||||
typeof bodyObj.error === "object"
|
||||
) {
|
||||
if (bodyObj.success === false && bodyObj.error && typeof bodyObj.error === "object") {
|
||||
const errorObj = bodyObj.error as Record<string, unknown>;
|
||||
const code = typeof errorObj.code === "string" ? errorObj.code : undefined;
|
||||
const message = typeof errorObj.message === "string" ? errorObj.message : undefined;
|
||||
@ -97,18 +90,8 @@ function parseApiError(error: ClientApiError): ParsedError {
|
||||
}
|
||||
}
|
||||
|
||||
// Try extracting message from body
|
||||
const extractedMessage = extractMessageFromBody(body);
|
||||
if (extractedMessage) {
|
||||
const code = matchErrorPattern(extractedMessage);
|
||||
const metadata = ErrorMetadata[code];
|
||||
return {
|
||||
code,
|
||||
message: extractedMessage,
|
||||
shouldLogout: metadata.shouldLogout,
|
||||
shouldRetry: metadata.shouldRetry,
|
||||
};
|
||||
}
|
||||
// No message-based code inference. If the response doesn't include a structured error code,
|
||||
// we fall back to status-based mapping below.
|
||||
}
|
||||
|
||||
// Fall back to status code mapping
|
||||
@ -146,46 +129,14 @@ function parseNativeError(error: Error): ParsedError {
|
||||
};
|
||||
}
|
||||
|
||||
// Try pattern matching on error message
|
||||
const code = matchErrorPattern(error.message);
|
||||
const metadata = ErrorMetadata[code];
|
||||
return {
|
||||
code,
|
||||
message: code === ErrorCode.UNKNOWN ? error.message : ErrorMessages[code],
|
||||
shouldLogout: metadata.shouldLogout,
|
||||
shouldRetry: metadata.shouldRetry,
|
||||
code: ErrorCode.UNKNOWN,
|
||||
message: error.message || ErrorMessages[ErrorCode.UNKNOWN],
|
||||
shouldLogout: false,
|
||||
shouldRetry: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract error message from response body
|
||||
*/
|
||||
function extractMessageFromBody(body: unknown): string | null {
|
||||
if (!body || typeof body !== "object") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bodyObj = body as Record<string, unknown>;
|
||||
|
||||
// Check nested error.message (standard format)
|
||||
if (bodyObj.error && typeof bodyObj.error === "object") {
|
||||
const errorObj = bodyObj.error as Record<string, unknown>;
|
||||
if (typeof errorObj.message === "string") {
|
||||
return errorObj.message;
|
||||
}
|
||||
}
|
||||
|
||||
// Check top-level message
|
||||
if (typeof bodyObj.message === "string") {
|
||||
return bodyObj.message;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map HTTP status code to error code
|
||||
*/
|
||||
function mapStatusToErrorCode(status?: number): ErrorCodeType {
|
||||
if (!status) return ErrorCode.UNKNOWN;
|
||||
|
||||
@ -249,6 +200,5 @@ export {
|
||||
ErrorCode,
|
||||
ErrorMessages,
|
||||
ErrorMetadata,
|
||||
matchErrorPattern,
|
||||
type ErrorCodeType,
|
||||
} from "@customer-portal/domain/common";
|
||||
|
||||
@ -109,16 +109,19 @@ export const ErrorMessages: Record<ErrorCodeType, string> = {
|
||||
[ErrorCode.CUSTOMER_NOT_FOUND]: "Customer account not found. Please contact support.",
|
||||
[ErrorCode.ORDER_ALREADY_PROCESSED]: "This order has already been processed.",
|
||||
[ErrorCode.INSUFFICIENT_BALANCE]: "Insufficient account balance.",
|
||||
[ErrorCode.SERVICE_UNAVAILABLE]: "This service is temporarily unavailable. Please try again later.",
|
||||
[ErrorCode.SERVICE_UNAVAILABLE]:
|
||||
"This service is temporarily unavailable. Please try again later.",
|
||||
|
||||
// System
|
||||
[ErrorCode.INTERNAL_ERROR]: "An unexpected error occurred. Please try again later.",
|
||||
[ErrorCode.EXTERNAL_SERVICE_ERROR]: "An external service is temporarily unavailable. Please try again later.",
|
||||
[ErrorCode.EXTERNAL_SERVICE_ERROR]:
|
||||
"An external service is temporarily unavailable. Please try again later.",
|
||||
[ErrorCode.DATABASE_ERROR]: "A system error occurred. Please try again later.",
|
||||
[ErrorCode.CONFIGURATION_ERROR]: "A system configuration error occurred. Please contact support.",
|
||||
|
||||
// Network
|
||||
[ErrorCode.NETWORK_ERROR]: "Unable to connect to the server. Please check your internet connection.",
|
||||
[ErrorCode.NETWORK_ERROR]:
|
||||
"Unable to connect to the server. Please check your internet connection.",
|
||||
[ErrorCode.TIMEOUT]: "The request timed out. Please try again.",
|
||||
[ErrorCode.RATE_LIMITED]: "Too many requests. Please wait a moment and try again.",
|
||||
|
||||
@ -162,7 +165,9 @@ export const ErrorMetadata: Record<ErrorCodeType, ErrorMetadata> = {
|
||||
severity: "low",
|
||||
shouldLogout: true,
|
||||
shouldRetry: false,
|
||||
logLevel: "info",
|
||||
// Session expiry is an expected flow (browser tabs, refresh loops, etc.) and can be extremely noisy.
|
||||
// Keep it available for debugging but avoid spamming production logs at info level.
|
||||
logLevel: "debug",
|
||||
},
|
||||
[ErrorCode.TOKEN_INVALID]: {
|
||||
category: "authentication",
|
||||
@ -378,68 +383,8 @@ export function canRetryError(code: string): boolean {
|
||||
return getErrorMetadata(code).shouldRetry;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Pattern Matching for Error Classification
|
||||
// ============================================================================
|
||||
|
||||
interface ErrorPattern {
|
||||
pattern: RegExp;
|
||||
code: ErrorCodeType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Patterns to match error messages to error codes.
|
||||
* Used when explicit error codes are not available.
|
||||
*/
|
||||
export const ErrorPatterns: ErrorPattern[] = [
|
||||
// Authentication patterns
|
||||
{ pattern: /invalid.*credentials?|wrong.*password|invalid.*password/i, code: ErrorCode.INVALID_CREDENTIALS },
|
||||
{ pattern: /account.*locked|locked.*account|too.*many.*attempts/i, code: ErrorCode.ACCOUNT_LOCKED },
|
||||
{ pattern: /session.*expired|expired.*session/i, code: ErrorCode.SESSION_EXPIRED },
|
||||
{ pattern: /token.*expired|expired.*token/i, code: ErrorCode.SESSION_EXPIRED },
|
||||
{ pattern: /token.*revoked|revoked.*token/i, code: ErrorCode.TOKEN_REVOKED },
|
||||
{ pattern: /invalid.*token|token.*invalid/i, code: ErrorCode.TOKEN_INVALID },
|
||||
{ pattern: /refresh.*token.*invalid|invalid.*refresh/i, code: ErrorCode.REFRESH_TOKEN_INVALID },
|
||||
|
||||
// Authorization patterns
|
||||
{ pattern: /admin.*required|requires?.*admin/i, code: ErrorCode.ADMIN_REQUIRED },
|
||||
{ pattern: /forbidden|not.*authorized|unauthorized/i, code: ErrorCode.FORBIDDEN },
|
||||
{ pattern: /access.*denied|permission.*denied/i, code: ErrorCode.RESOURCE_ACCESS_DENIED },
|
||||
|
||||
// Business patterns
|
||||
{ pattern: /already.*exists|email.*exists|account.*exists/i, code: ErrorCode.ACCOUNT_EXISTS },
|
||||
{ pattern: /already.*linked/i, code: ErrorCode.ACCOUNT_ALREADY_LINKED },
|
||||
{ pattern: /customer.*not.*found|account.*not.*found/i, code: ErrorCode.CUSTOMER_NOT_FOUND },
|
||||
{ pattern: /already.*processed/i, code: ErrorCode.ORDER_ALREADY_PROCESSED },
|
||||
{ pattern: /insufficient.*balance/i, code: ErrorCode.INSUFFICIENT_BALANCE },
|
||||
|
||||
// System patterns
|
||||
{ pattern: /database|sql|postgres|prisma|connection.*refused/i, code: ErrorCode.DATABASE_ERROR },
|
||||
{ pattern: /whmcs|salesforce|external.*service/i, code: ErrorCode.EXTERNAL_SERVICE_ERROR },
|
||||
{ pattern: /configuration.*error|missing.*config/i, code: ErrorCode.CONFIGURATION_ERROR },
|
||||
|
||||
// Network patterns
|
||||
{ pattern: /network.*error|fetch.*failed|econnrefused/i, code: ErrorCode.NETWORK_ERROR },
|
||||
{ pattern: /timeout|timed?\s*out/i, code: ErrorCode.TIMEOUT },
|
||||
{ pattern: /too.*many.*requests|rate.*limit/i, code: ErrorCode.RATE_LIMITED },
|
||||
|
||||
// Validation patterns (lower priority - checked last)
|
||||
{ pattern: /not.*found/i, code: ErrorCode.NOT_FOUND },
|
||||
{ pattern: /validation.*failed|invalid/i, code: ErrorCode.VALIDATION_FAILED },
|
||||
{ pattern: /required|missing/i, code: ErrorCode.REQUIRED_FIELD_MISSING },
|
||||
];
|
||||
|
||||
/**
|
||||
* Match an error message to an error code using patterns
|
||||
*/
|
||||
export function matchErrorPattern(message: string): ErrorCodeType {
|
||||
for (const { pattern, code } of ErrorPatterns) {
|
||||
if (pattern.test(message)) {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
return ErrorCode.UNKNOWN;
|
||||
}
|
||||
// NOTE: We intentionally do NOT support matching error messages to codes.
|
||||
// Error codes must be explicit and stable (returned from the API or thrown by server code).
|
||||
|
||||
// ============================================================================
|
||||
// Zod Schema for Error Response
|
||||
@ -476,4 +421,3 @@ export function createErrorResponse(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user