Merge pull request #2810 from AnvayKharb/WEB-429-implement-centralized-error-handler-service-for-user-friendly-api-error-messaging
Web 429 implement centralized error handler service for user friendly api error messaging
This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
# Error Handler Service
|
||||
|
||||
## Overview
|
||||
|
||||
The `ErrorHandlerService` is a centralized error-handling service for the Mifos X Web App. It provides consistent, user-friendly error messaging across the application by converting HTTP errors into meaningful messages and displaying them through Material Design snackbars.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Trigger Mechanism
|
||||
|
||||
The `ErrorHandlerService` is **manually triggered** by components and services using RxJS's `catchError` operator. It works **alongside** (not replacing) the existing `ErrorHandlerInterceptor`:
|
||||
|
||||
#### Existing Interceptor (`error-handler.interceptor.ts`)
|
||||
|
||||
- Automatically intercepts **all** HTTP errors globally
|
||||
- Shows generic error dialog for unhandled errors
|
||||
- Cannot provide context-specific messages
|
||||
- Located at: `src/app/core/http/error-handler.interceptor.ts`
|
||||
|
||||
#### ErrorHandlerService (This Service)
|
||||
|
||||
- **Manually invoked** per API call
|
||||
- Provides **context-specific** error messages
|
||||
- Displays user-friendly snackbar notifications
|
||||
- Extracts Fineract-specific error details
|
||||
- Gives developers fine-grained control
|
||||
|
||||
### When to Use
|
||||
|
||||
Use `ErrorHandlerService` when you need:
|
||||
|
||||
- Custom error messages for specific operations
|
||||
- Context-aware error handling (e.g., "User not found", "Loan approval failed")
|
||||
- Snackbar notifications instead of modal dialogs
|
||||
- To extract and display Fineract API error messages
|
||||
|
||||
The interceptor still catches errors you don't explicitly handle, providing a safety net.
|
||||
|
||||
## Problem Solved
|
||||
|
||||
**Before:** The app handled API errors inconsistently:
|
||||
|
||||
- Some errors were shown directly from the server
|
||||
- Others were not displayed clearly to users
|
||||
- Error handling code was duplicated across components
|
||||
- Poor user experience with technical error messages
|
||||
|
||||
**After:** Centralized error handling with:
|
||||
|
||||
- Consistent error messaging
|
||||
- User-friendly error descriptions
|
||||
- Reusable service across all components
|
||||
- Better UX with appropriate snackbar notifications
|
||||
|
||||
## Features
|
||||
|
||||
### 1. HTTP Error Handling
|
||||
|
||||
- Automatically converts HTTP status codes into user-friendly messages
|
||||
- Extracts Fineract-specific error messages
|
||||
- Handles network/connection errors
|
||||
- Supports contextual error messages
|
||||
|
||||
### 2. Supported Error Codes
|
||||
|
||||
| Status Code | Title | Behavior |
|
||||
| ------------- | ------------------- | ----------------------- |
|
||||
| 400 | Invalid Request | Shows validation errors |
|
||||
| 401 | Unauthorized | Session expired message |
|
||||
| 403 | Access Denied | Permission error |
|
||||
| 404 | Not Found | Resource not found |
|
||||
| 409 | Conflict | Duplicate resource |
|
||||
| 500 | Server Error | Generic server error |
|
||||
| 503 | Service Unavailable | Service down message |
|
||||
| Network Error | Connection Error | Connection issues |
|
||||
|
||||
### 3. Notification Types
|
||||
|
||||
- **Error Notifications**: Red snackbar, top-center, 5 seconds
|
||||
- **Success Notifications**: Green snackbar, bottom-center, 3 seconds
|
||||
- **Info Notifications**: Blue snackbar, bottom-center, 4 seconds
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Error Handling
|
||||
|
||||
```typescript
|
||||
import { ErrorHandlerService } from '@core/error-handler/error-handler.service';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
export class MyComponent {
|
||||
constructor(private errorHandler: ErrorHandlerService) {}
|
||||
|
||||
loadData() {
|
||||
this.dataService
|
||||
.getData()
|
||||
.pipe(catchError((error) => this.errorHandler.handleError(error)))
|
||||
.subscribe((data) => {
|
||||
// Handle success
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Handling with Context
|
||||
|
||||
```typescript
|
||||
loadUser(userId: string) {
|
||||
this.userService.getUser(userId).pipe(
|
||||
catchError(error => this.errorHandler.handleError(error, 'User'))
|
||||
).subscribe(user => {
|
||||
// Handle success
|
||||
});
|
||||
}
|
||||
// Shows: "Not Found: User not found." for 404 errors
|
||||
```
|
||||
|
||||
### Success Messages
|
||||
|
||||
```typescript
|
||||
saveData() {
|
||||
this.dataService.save(data).subscribe(
|
||||
response => {
|
||||
this.errorHandler.showSuccess('Data saved successfully!');
|
||||
},
|
||||
error => {
|
||||
this.errorHandler.handleError(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Info Messages
|
||||
|
||||
```typescript
|
||||
loadData() {
|
||||
this.errorHandler.showInfo('Loading data, please wait...');
|
||||
this.dataService.getData().subscribe(/* ... */);
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Custom Error Handling
|
||||
|
||||
```typescript
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
|
||||
processData() {
|
||||
this.dataService.process().subscribe(
|
||||
response => {
|
||||
this.errorHandler.showSuccess('Processing complete!');
|
||||
},
|
||||
(error: HttpErrorResponse) => {
|
||||
if (error.status === 409) {
|
||||
// Custom handling for conflicts
|
||||
this.handleConflict(error);
|
||||
} else {
|
||||
this.errorHandler.handleError(error, 'Data processing');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Integration with Form Validation
|
||||
|
||||
```typescript
|
||||
submitForm() {
|
||||
if (this.form.invalid) {
|
||||
this.errorHandler.showInfo('Please fill all required fields');
|
||||
return;
|
||||
}
|
||||
|
||||
this.formService.submit(this.form.value).pipe(
|
||||
catchError(error => this.errorHandler.handleError(error, 'Form submission'))
|
||||
).subscribe(response => {
|
||||
this.errorHandler.showSuccess('Form submitted successfully!');
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
The service uses three CSS classes defined in `error-handler.component.scss`:
|
||||
|
||||
```scss
|
||||
.error-snackbar // Red background (#f44336) for errors
|
||||
.success-snackbar // Green background (#4caf50) for success
|
||||
.info-snackbar // Blue background (#2196f3) for info
|
||||
```
|
||||
|
||||
These styles are automatically applied based on the notification type. The styles are imported globally in `main.scss` so they work across the entire application.
|
||||
|
||||
## API Reference
|
||||
|
||||
### Methods
|
||||
|
||||
#### `handleError(error: HttpErrorResponse, context?: string): Observable<never>`
|
||||
|
||||
Handles HTTP errors and shows user-friendly messages.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `error`: The HTTP error response
|
||||
- `context` (optional): Additional context for more specific messages
|
||||
|
||||
**Returns:** Observable that throws the original error
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
this.errorHandler.handleError(error, 'Client');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `showSuccess(message: string, action?: string): void`
|
||||
|
||||
Shows a success message to the user.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `message`: The success message to display
|
||||
- `action` (optional): Button text (defaults to 'OK')
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
this.errorHandler.showSuccess('Client created successfully!', 'View');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### `showInfo(message: string, action?: string): void`
|
||||
|
||||
Shows an informational message to the user.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `message`: The info message to display
|
||||
- `action` (optional): Button text (defaults to 'OK')
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
this.errorHandler.showInfo('Loading data...', 'Dismiss');
|
||||
```
|
||||
|
||||
## Fineract Integration
|
||||
|
||||
The service automatically extracts Fineract-specific error messages:
|
||||
|
||||
```typescript
|
||||
// Fineract error structure
|
||||
{
|
||||
"errors": [{
|
||||
"defaultUserMessage": "Client with same name already exists"
|
||||
}],
|
||||
"defaultUserMessage": "Validation error"
|
||||
}
|
||||
```
|
||||
|
||||
The service prioritizes:
|
||||
|
||||
1. `errors[0].defaultUserMessage`
|
||||
2. `defaultUserMessage`
|
||||
3. Generic fallback message
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Before (Old Approach)
|
||||
|
||||
```typescript
|
||||
// Inconsistent error handling
|
||||
this.clientService.getClient(id).subscribe(
|
||||
(data) => {
|
||||
/* success */
|
||||
},
|
||||
(error) => {
|
||||
console.error(error);
|
||||
alert('Error loading client'); // Poor UX
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### After (Centralized Approach)
|
||||
|
||||
```typescript
|
||||
// Consistent error handling
|
||||
this.clientService
|
||||
.getClient(id)
|
||||
.pipe(catchError((error) => this.errorHandler.handleError(error, 'Client')))
|
||||
.subscribe((data) => {
|
||||
// Handle success
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always provide context** for better error messages:
|
||||
|
||||
```typescript
|
||||
catchError((error) => this.errorHandler.handleError(error, 'Loan Application'));
|
||||
```
|
||||
|
||||
2. **Use appropriate notification types**:
|
||||
- Errors: Use `handleError()` for API failures
|
||||
- Success: Use `showSuccess()` for successful operations
|
||||
- Info: Use `showInfo()` for informational messages
|
||||
|
||||
3. **Don't duplicate error handling**: Let the service handle standard errors
|
||||
|
||||
4. **Custom handling when needed**: Handle specific error cases before using the service
|
||||
|
||||
5. **Combine with loading states**:
|
||||
```typescript
|
||||
this.isLoading = true;
|
||||
this.service
|
||||
.getData()
|
||||
.pipe(
|
||||
finalize(() => (this.isLoading = false)),
|
||||
catchError((error) => this.errorHandler.handleError(error))
|
||||
)
|
||||
.subscribe();
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```typescript
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Router } from '@angular/router';
|
||||
import { ErrorHandlerService } from './error-handler.service';
|
||||
|
||||
describe('ErrorHandlerService', () => {
|
||||
let service: ErrorHandlerService;
|
||||
let snackBar: jasmine.SpyObj<MatSnackBar>;
|
||||
|
||||
beforeEach(() => {
|
||||
const snackBarSpy = jasmine.createSpyObj('MatSnackBar', ['open']);
|
||||
const routerSpy = jasmine.createSpyObj('Router', ['navigate']);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
ErrorHandlerService,
|
||||
{ provide: MatSnackBar, useValue: snackBarSpy },
|
||||
{ provide: Router, useValue: routerSpy }]
|
||||
});
|
||||
|
||||
service = TestBed.inject(ErrorHandlerService);
|
||||
snackBar = TestBed.inject(MatSnackBar) as jasmine.SpyObj<MatSnackBar>;
|
||||
});
|
||||
|
||||
it('should show success message', () => {
|
||||
service.showSuccess('Test success');
|
||||
expect(snackBar.open).toHaveBeenCalledWith(
|
||||
'Test success',
|
||||
'OK',
|
||||
jasmine.objectContaining({
|
||||
panelClass: ['success-snackbar']
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Related Issues
|
||||
|
||||
- **WEB-429**: Implement centralized error handler service for user-friendly API error messaging
|
||||
|
||||
## Contributing
|
||||
|
||||
When modifying this service:
|
||||
|
||||
1. Ensure all HTTP status codes are handled appropriately
|
||||
2. Update the documentation with new features
|
||||
3. Maintain consistent snackbar positioning and duration
|
||||
4. Test with actual Fineract API responses
|
||||
5. Follow Angular and TypeScript best practices
|
||||
|
||||
## License
|
||||
|
||||
Licensed under the Apache License, Version 2.0. See the LICENSE file for details.
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/* === Snackbar Styles for Error Handler Service === */
|
||||
|
||||
/**
|
||||
* Error notification snackbar styling
|
||||
* - Red background for errors
|
||||
* - White text for contrast
|
||||
* - Displayed at top-center for 5 seconds
|
||||
*/
|
||||
.error-snackbar {
|
||||
background-color: #f44336 !important;
|
||||
color: white !important;
|
||||
|
||||
.mat-mdc-snack-bar-label,
|
||||
.mdc-snackbar__label,
|
||||
.mat-simple-snackbar-action {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.mat-mdc-button {
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Success notification snackbar styling
|
||||
* - Green background for success messages
|
||||
* - White text for contrast
|
||||
* - Displayed at bottom-center for 3 seconds
|
||||
*/
|
||||
.success-snackbar {
|
||||
background-color: #4caf50 !important;
|
||||
color: white !important;
|
||||
|
||||
.mat-mdc-snack-bar-label,
|
||||
.mdc-snackbar__label,
|
||||
.mat-simple-snackbar-action {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.mat-mdc-button {
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Info notification snackbar styling
|
||||
* - Blue background for informational messages
|
||||
* - White text for contrast
|
||||
* - Displayed at bottom-center for 4 seconds
|
||||
*/
|
||||
.info-snackbar {
|
||||
background-color: #2196f3 !important;
|
||||
color: white !important;
|
||||
|
||||
.mat-mdc-snack-bar-label,
|
||||
.mdc-snackbar__label,
|
||||
.mat-simple-snackbar-action {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.mat-mdc-button {
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
export interface ErrorMessage {
|
||||
title: string;
|
||||
message: string;
|
||||
action?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Centralized error handler service for consistent error messaging
|
||||
* across the application. Converts HTTP errors into user-friendly
|
||||
* messages and displays them through snackbar notifications.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ErrorHandlerService {
|
||||
constructor(
|
||||
private snackBar: MatSnackBar,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Handle HTTP errors and show user-friendly messages
|
||||
* @param error The HTTP error response
|
||||
* @param context Optional context to provide more specific error messages
|
||||
* @returns Observable that throws the original error
|
||||
*/
|
||||
handleError(error: HttpErrorResponse, context?: string): Observable<never> {
|
||||
const errorMessage = this.getErrorMessage(error, context);
|
||||
this.showError(errorMessage);
|
||||
return throwError(() => error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user-friendly error message based on HTTP status
|
||||
* @param error The HTTP error response
|
||||
* @param context Optional context for more specific messages
|
||||
* @returns ErrorMessage object with title, message, and action
|
||||
*/
|
||||
private getErrorMessage(error: HttpErrorResponse, context?: string): ErrorMessage {
|
||||
if (error.error instanceof ErrorEvent) {
|
||||
// Client-side or network error
|
||||
return {
|
||||
title: 'Connection Error',
|
||||
message: 'Unable to connect to the server. Please check your internet connection.',
|
||||
action: 'OK'
|
||||
};
|
||||
}
|
||||
|
||||
// Server-side error - Extract Fineract-specific error messages
|
||||
const fineractError = error.error?.errors?.[0]?.defaultUserMessage;
|
||||
const defaultMessage = error.error?.defaultUserMessage;
|
||||
|
||||
switch (error.status) {
|
||||
case 400:
|
||||
return {
|
||||
title: 'Invalid Request',
|
||||
message: fineractError || defaultMessage || 'Please check your input and try again.',
|
||||
action: 'OK'
|
||||
};
|
||||
case 401:
|
||||
return {
|
||||
title: 'Unauthorized',
|
||||
message: 'Your session has expired. Please log in again.',
|
||||
action: 'Login'
|
||||
};
|
||||
case 403:
|
||||
return {
|
||||
title: 'Access Denied',
|
||||
message: fineractError || defaultMessage || 'You do not have permission to perform this action.',
|
||||
action: 'OK'
|
||||
};
|
||||
case 404:
|
||||
return {
|
||||
title: 'Not Found',
|
||||
message: context
|
||||
? `${context} not found.`
|
||||
: fineractError || defaultMessage || 'The requested resource was not found.',
|
||||
action: 'OK'
|
||||
};
|
||||
case 409:
|
||||
return {
|
||||
title: 'Conflict',
|
||||
message: fineractError || defaultMessage || 'The resource already exists or there is a conflict.',
|
||||
action: 'OK'
|
||||
};
|
||||
case 500:
|
||||
return {
|
||||
title: 'Server Error',
|
||||
message: fineractError || defaultMessage || 'An unexpected error occurred. Please try again later.',
|
||||
action: 'OK'
|
||||
};
|
||||
case 503:
|
||||
return {
|
||||
title: 'Service Unavailable',
|
||||
message: 'The service is temporarily unavailable. Please try again later.',
|
||||
action: 'OK'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
title: 'Error',
|
||||
message: fineractError || defaultMessage || 'An unexpected error occurred.',
|
||||
action: 'OK'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show error message to user via snackbar
|
||||
* @param errorMessage The error message object to display
|
||||
*/
|
||||
private showError(errorMessage: ErrorMessage): void {
|
||||
const snackBarRef = this.snackBar.open(
|
||||
`${errorMessage.title}: ${errorMessage.message}`,
|
||||
errorMessage.action || 'Close',
|
||||
{
|
||||
duration: 5000,
|
||||
horizontalPosition: 'center',
|
||||
verticalPosition: 'top',
|
||||
panelClass: ['error-snackbar']
|
||||
}
|
||||
);
|
||||
|
||||
// Handle action button clicks (e.g., "Login" for 401 errors)
|
||||
if (errorMessage.action === 'Login') {
|
||||
snackBarRef.onAction().subscribe(() => {
|
||||
this.router.navigate(['/login']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show success message to user via snackbar
|
||||
* @param message The success message to display
|
||||
* @param action Optional action button text (defaults to 'OK')
|
||||
*/
|
||||
showSuccess(message: string, action: string = 'OK'): void {
|
||||
this.snackBar.open(message, action, {
|
||||
duration: 3000,
|
||||
horizontalPosition: 'center',
|
||||
verticalPosition: 'bottom',
|
||||
panelClass: ['success-snackbar']
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show info message to user via snackbar
|
||||
* @param message The info message to display
|
||||
* @param action Optional action button text (defaults to 'OK')
|
||||
*/
|
||||
showInfo(message: string, action: string = 'OK'): void {
|
||||
this.snackBar.open(message, action, {
|
||||
duration: 4000,
|
||||
horizontalPosition: 'center',
|
||||
verticalPosition: 'bottom',
|
||||
panelClass: ['info-snackbar']
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,9 @@
|
||||
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
|
||||
// Error Handler Service Styles
|
||||
@use 'app/core/error-handler/error-handler.component';
|
||||
|
||||
.scrollbar-styling {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #889 #f1f1f1;
|
||||
|
||||
Reference in New Issue
Block a user