Spaces:
Building
Building
// 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; | |
} | |
({ | |
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'; | |
() | |
export class ErrorInterceptor implements HttpInterceptor { | |
private activeRequests = new Map<string, AbortController>(); | |
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { | |
// 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(); | |
} | |
} |