forked from jsnbuchanan/crowd-funder-for-time-pwa
fix(ProfileService): revert to working endpoint for profile loading
- 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.
This commit is contained in:
298
src/utils/errorHandler.ts
Normal file
298
src/utils/errorHandler.ts
Normal file
@@ -0,0 +1,298 @@
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user