- Revert ProfileService from broken /api/partner/userProfile endpoint to working /api/partner/userProfileForIssuer/${did}
- Fix location data display by restoring single profile object response parsing
- Remove complex array handling logic that was unnecessary for current user profiles
- Restore original working functionality that was broken by recent refactoring
Problem: Recent ProfileService creation changed endpoint from working userProfileForIssuer/${did}
to broken userProfile (list endpoint), causing location data to not display properly.
Solution: Revert to original working endpoint and response parsing logic that returns
single profile objects with location data instead of arrays of all profiles.
Files changed:
- src/services/ProfileService.ts: Restore working endpoint and simplify response parsing
Testing: Profile loading now works correctly for both existing and new profiles,
location data is properly extracted and displayed, maps render correctly.
299 lines
7.5 KiB
TypeScript
299 lines
7.5 KiB
TypeScript
/**
|
|
* Standardized Error Handler
|
|
*
|
|
* Provides consistent error handling patterns across the TimeSafari codebase
|
|
* to improve debugging, user experience, and maintainability.
|
|
*
|
|
* @author Matthew Raymer
|
|
* @since 2025-08-25
|
|
*/
|
|
|
|
import { AxiosError } from "axios";
|
|
import { logger } from "./logger";
|
|
|
|
/**
|
|
* Standard error context for consistent logging
|
|
*/
|
|
export interface ErrorContext {
|
|
component: string;
|
|
operation: string;
|
|
timestamp: string;
|
|
[key: string]: unknown;
|
|
}
|
|
|
|
/**
|
|
* Enhanced error information for better debugging
|
|
*/
|
|
export interface EnhancedErrorInfo {
|
|
errorType: "AxiosError" | "NetworkError" | "ValidationError" | "UnknownError";
|
|
status?: number;
|
|
statusText?: string;
|
|
errorData?: unknown;
|
|
errorMessage: string;
|
|
errorStack?: string;
|
|
requestContext?: {
|
|
url?: string;
|
|
method?: string;
|
|
headers?: Record<string, unknown>;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Standardized error handler for API operations
|
|
*
|
|
* @param error - The error that occurred
|
|
* @param context - Context information about the operation
|
|
* @param operation - Description of the operation being performed
|
|
* @returns Enhanced error information for consistent handling
|
|
*/
|
|
export function handleApiError(
|
|
error: unknown,
|
|
context: ErrorContext,
|
|
operation: string,
|
|
): EnhancedErrorInfo {
|
|
const baseContext = {
|
|
...context,
|
|
operation,
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
|
|
if (error instanceof AxiosError) {
|
|
const axiosError = error as AxiosError;
|
|
const status = axiosError.response?.status;
|
|
const statusText = axiosError.response?.statusText;
|
|
const errorData = axiosError.response?.data;
|
|
|
|
const enhancedError: EnhancedErrorInfo = {
|
|
errorType: "AxiosError",
|
|
status,
|
|
statusText,
|
|
errorData,
|
|
errorMessage: axiosError.message,
|
|
errorStack: axiosError.stack,
|
|
requestContext: {
|
|
url: axiosError.config?.url,
|
|
method: axiosError.config?.method,
|
|
headers: axiosError.config?.headers,
|
|
},
|
|
};
|
|
|
|
// Log with consistent format
|
|
logger.error(
|
|
`[${context.component}] ❌ ${operation} failed (AxiosError):`,
|
|
{
|
|
...baseContext,
|
|
...enhancedError,
|
|
},
|
|
);
|
|
|
|
return enhancedError;
|
|
}
|
|
|
|
if (error instanceof Error) {
|
|
const enhancedError: EnhancedErrorInfo = {
|
|
errorType: "UnknownError",
|
|
errorMessage: error.message,
|
|
errorStack: error.stack,
|
|
};
|
|
|
|
logger.error(`[${context.component}] ❌ ${operation} failed (Error):`, {
|
|
...baseContext,
|
|
...enhancedError,
|
|
});
|
|
|
|
return enhancedError;
|
|
}
|
|
|
|
// Handle unknown error types
|
|
const enhancedError: EnhancedErrorInfo = {
|
|
errorType: "UnknownError",
|
|
errorMessage: String(error),
|
|
};
|
|
|
|
logger.error(`[${context.component}] ❌ ${operation} failed (Unknown):`, {
|
|
...baseContext,
|
|
...enhancedError,
|
|
});
|
|
|
|
return enhancedError;
|
|
}
|
|
|
|
/**
|
|
* Extract human-readable error message from various error response formats
|
|
*
|
|
* @param errorData - Error response data
|
|
* @returns Human-readable error message
|
|
*/
|
|
export function extractErrorMessage(errorData: unknown): string {
|
|
if (typeof errorData === "string") {
|
|
return errorData;
|
|
}
|
|
|
|
if (typeof errorData === "object" && errorData !== null) {
|
|
const obj = errorData as Record<string, unknown>;
|
|
|
|
// Try common error message fields
|
|
if (obj.message && typeof obj.message === "string") {
|
|
return obj.message;
|
|
}
|
|
|
|
if (obj.error && typeof obj.error === "string") {
|
|
return obj.error;
|
|
}
|
|
|
|
if (obj.detail && typeof obj.detail === "string") {
|
|
return obj.detail;
|
|
}
|
|
|
|
if (obj.reason && typeof obj.reason === "string") {
|
|
return obj.reason;
|
|
}
|
|
|
|
// Fallback to stringified object
|
|
return JSON.stringify(errorData);
|
|
}
|
|
|
|
return String(errorData);
|
|
}
|
|
|
|
/**
|
|
* Create user-friendly error message from enhanced error info
|
|
*
|
|
* @param errorInfo - Enhanced error information
|
|
* @param fallbackMessage - Fallback message if error details are insufficient
|
|
* @returns User-friendly error message
|
|
*/
|
|
export function createUserMessage(
|
|
errorInfo: EnhancedErrorInfo,
|
|
fallbackMessage: string,
|
|
): string {
|
|
if (errorInfo.errorType === "AxiosError") {
|
|
const status = errorInfo.status;
|
|
const statusText = errorInfo.statusText;
|
|
const errorMessage = extractErrorMessage(errorInfo.errorData);
|
|
|
|
if (status && statusText) {
|
|
if (errorMessage && errorMessage !== "{}") {
|
|
return `${fallbackMessage}: ${status} ${statusText} - ${errorMessage}`;
|
|
}
|
|
return `${fallbackMessage}: ${status} ${statusText}`;
|
|
}
|
|
}
|
|
|
|
if (
|
|
errorInfo.errorMessage &&
|
|
errorInfo.errorMessage !== "Request failed with status code 0"
|
|
) {
|
|
return `${fallbackMessage}: ${errorInfo.errorMessage}`;
|
|
}
|
|
|
|
return fallbackMessage;
|
|
}
|
|
|
|
/**
|
|
* Handle specific HTTP status codes with appropriate user messages
|
|
*
|
|
* @param status - HTTP status code
|
|
* @param errorData - Error response data
|
|
* @param operation - Description of the operation
|
|
* @returns User-friendly error message
|
|
*/
|
|
export function handleHttpStatus(
|
|
status: number,
|
|
errorData: unknown,
|
|
operation: string,
|
|
): string {
|
|
const errorMessage = extractErrorMessage(errorData);
|
|
|
|
switch (status) {
|
|
case 400:
|
|
return errorMessage || `${operation} failed: Bad request`;
|
|
case 401:
|
|
return `${operation} failed: Authentication required`;
|
|
case 403:
|
|
return `${operation} failed: Access denied`;
|
|
case 404:
|
|
return errorMessage || `${operation} failed: Resource not found`;
|
|
case 409:
|
|
return errorMessage || `${operation} failed: Conflict with existing data`;
|
|
case 422:
|
|
return errorMessage || `${operation} failed: Validation error`;
|
|
case 429:
|
|
return `${operation} failed: Too many requests. Please try again later.`;
|
|
case 500:
|
|
return `${operation} failed: Server error. Please try again later.`;
|
|
case 502:
|
|
case 503:
|
|
case 504:
|
|
return `${operation} failed: Service temporarily unavailable. Please try again later.`;
|
|
default:
|
|
return errorMessage || `${operation} failed: HTTP ${status}`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if an error is a network-related error
|
|
*
|
|
* @param error - The error to check
|
|
* @returns True if the error is network-related
|
|
*/
|
|
export function isNetworkError(error: unknown): boolean {
|
|
if (error instanceof AxiosError) {
|
|
return !error.response && !error.request;
|
|
}
|
|
|
|
if (error instanceof Error) {
|
|
const message = error.message.toLowerCase();
|
|
return (
|
|
message.includes("network") ||
|
|
message.includes("timeout") ||
|
|
message.includes("connection") ||
|
|
message.includes("fetch")
|
|
);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Check if an error is a timeout error
|
|
*
|
|
* @param error - The error to check
|
|
* @returns True if the error is a timeout
|
|
*/
|
|
export function isTimeoutError(error: unknown): boolean {
|
|
if (error instanceof AxiosError) {
|
|
return (
|
|
error.code === "ECONNABORTED" ||
|
|
error.message.toLowerCase().includes("timeout")
|
|
);
|
|
}
|
|
|
|
if (error instanceof Error) {
|
|
return error.message.toLowerCase().includes("timeout");
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Create standardized error context for components
|
|
*
|
|
* @param component - Component name
|
|
* @param operation - Operation being performed
|
|
* @param additionalContext - Additional context information
|
|
* @returns Standardized error context
|
|
*/
|
|
export function createErrorContext(
|
|
component: string,
|
|
operation: string,
|
|
additionalContext: Record<string, unknown> = {},
|
|
): ErrorContext {
|
|
return {
|
|
component,
|
|
operation,
|
|
timestamp: new Date().toISOString(),
|
|
...additionalContext,
|
|
};
|
|
}
|