// error-handler.service.ts // Path: /flare-ui/src/app/services/error-handler.service.ts import { ErrorHandler, Injectable, Injector } from '@angular/core'; import { MatSnackBar } from '@angular/material/snack-bar'; import { Router } from '@angular/router'; import { HttpErrorResponse } from '@angular/common/http'; interface FlareError { error: string; message: string; details?: any; request_id?: string; timestamp?: string; user_action?: string; } @Injectable({ providedIn: 'root' }) export class GlobalErrorHandler implements ErrorHandler { constructor(private injector: Injector) {} handleError(error: Error | HttpErrorResponse): void { try { // Get services lazily to avoid circular dependency const snackBar = this.injector.get(MatSnackBar); const router = this.injector.get(Router); console.error('Global error caught:', error); // Handle HTTP errors if (error instanceof HttpErrorResponse) { this.handleHttpError(error, snackBar, router); } else { // Handle client-side errors this.handleClientError(error, snackBar); } } catch (handlerError) { // Fallback if error handler itself fails console.error('Error in error handler:', handlerError); console.error('Original error:', error); } } private handleHttpError(error: HttpErrorResponse, snackBar: MatSnackBar, router: Router): void { try { const flareError = error.error as FlareError; // Race condition error (409) if (error.status === 409) { const isRaceCondition = flareError?.error === 'RaceConditionError' || error.error?.type === 'race_condition'; if (isRaceCondition) { const snackBarRef = snackBar.open( flareError?.message || 'The data was modified by another user. Please refresh and try again.', 'Refresh', { duration: 0, panelClass: ['error-snackbar', 'race-condition-snackbar'] } ); snackBarRef.onAction().subscribe(() => { window.location.reload(); }); // Show additional info if available if (flareError?.details?.last_update_user) { console.info(`Last updated by: ${flareError.details.last_update_user} at ${flareError.details.last_update_date}`); } return; } } // Authentication error (401) if (error.status === 401) { snackBar.open( 'Your session has expired. Please login again.', 'Login', { duration: 5000, panelClass: ['error-snackbar'] } ).onAction().subscribe(() => { router.navigate(['/login']); }); return; } // Validation error (422) if (error.status === 422 && flareError?.details) { const fieldErrors = Array.isArray(flareError.details) ? flareError.details.map((d: any) => `${d.field}: ${d.message}`).join('\n') : 'Validation error occurred'; snackBar.open( flareError.message || 'Validation failed. Please check your input.', 'Close', { duration: 8000, panelClass: ['error-snackbar', 'validation-snackbar'] } ); console.error('Validation errors:', flareError.details); return; } // Not found error (404) if (error.status === 404) { snackBar.open( flareError?.message || 'The requested resource was not found.', 'Close', { duration: 5000, panelClass: ['error-snackbar'] } ); return; } // Server errors (5xx) if (error.status >= 500) { const message = flareError?.message || 'A server error occurred. Please try again later.'; const requestId = flareError?.request_id || error.headers?.get('X-Request-ID'); snackBar.open( requestId ? `${message} (Request ID: ${requestId})` : message, 'Close', { duration: 8000, panelClass: ['error-snackbar', 'server-error-snackbar'] } ); return; } // Network error (0 status usually indicates network issues) if (error.status === 0) { snackBar.open( 'Network connection error. Please check your internet connection.', 'Retry', { duration: 0, panelClass: ['error-snackbar', 'network-error-snackbar'] } ).onAction().subscribe(() => { window.location.reload(); }); return; } // Generic HTTP error const errorMessage = flareError?.message || error.message || `HTTP Error ${error.status}: ${error.statusText}`; snackBar.open( errorMessage, 'Close', { duration: 6000, panelClass: ['error-snackbar'] } ); } catch (err) { console.error('Error in handleHttpError:', err); this.showGenericError(snackBar); } } private handleClientError(error: Error, snackBar: MatSnackBar): void { try { // Check if it's a network error if (error.message?.includes('NetworkError') || error.message?.includes('Failed to fetch')) { snackBar.open( 'Network connection error. Please check your internet connection.', 'Retry', { duration: 0, panelClass: ['error-snackbar', 'network-error-snackbar'] } ).onAction().subscribe(() => { window.location.reload(); }); return; } // Check for specific Angular errors if (error.name === 'HttpErrorResponse') { // This might be an HTTP error that wasn't caught properly this.handleHttpError(error as any, snackBar, this.injector.get(Router)); return; } // Generic client error snackBar.open( 'An unexpected error occurred. Please refresh the page.', 'Refresh', { duration: 6000, panelClass: ['error-snackbar'] } ).onAction().subscribe(() => { window.location.reload(); }); } catch (err) { console.error('Error in handleClientError:', err); this.showGenericError(snackBar); } } private showGenericError(snackBar: MatSnackBar): void { snackBar.open( 'An error occurred. Please try again.', 'Close', { duration: 5000, panelClass: ['error-snackbar'] } ); } } // Error interceptor for consistent error format import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http'; import { Observable, throwError } from 'rxjs'; import { catchError, finalize } from 'rxjs/operators'; @Injectable() export class ErrorInterceptor implements HttpInterceptor { private activeRequests = new Map(); intercept(req: HttpRequest, next: HttpHandler): Observable> { // Create abort controller for request cancellation const requestId = this.generateRequestId(); const abortController = new AbortController(); this.activeRequests.set(requestId, abortController); // Clone request with additional headers const clonedReq = req.clone({ setHeaders: { 'X-Request-ID': requestId } }); return next.handle(clonedReq).pipe( catchError((error: HttpErrorResponse) => { // Log request details for debugging console.error('HTTP Error:', { url: req.url, method: req.method, status: error.status, statusText: error.statusText, error: error.error, requestId: requestId, headers: error.headers?.keys() }); // Enhanced error object const enhancedError = { ...error, requestId: requestId, timestamp: new Date().toISOString(), url: req.url, method: req.method }; // Re-throw to be handled by global error handler return throwError(() => enhancedError); }), finalize(() => { // Clean up abort controller this.activeRequests.delete(requestId); }) ); } private generateRequestId(): string { return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } // Method to cancel a specific request cancelRequest(requestId: string): void { const controller = this.activeRequests.get(requestId); if (controller) { controller.abort(); this.activeRequests.delete(requestId); } } // Method to cancel all active requests cancelAllRequests(): void { this.activeRequests.forEach((controller) => { controller.abort(); }); this.activeRequests.clear(); } }