fix(http): stop surfacing background/enrichment-fetch failures as error toasts
Two related cases where ErrorHandlerInterceptor's global toast fired even though the calling component already recovers gracefully on its own: - GET /kyc/risk-rules 404ing (e.g. not yet deployed on a given backend) popped a raw "Http failure response ... 404" toast on the Create Client page, even though client-general-step already falls back to an empty rule set and a default risk category when this call fails. - The Clients list page fires several per-row background calls (client data/KYC/address, for every visible row) plus the main search — each already had its own catchError/error handling, but the interceptor's 500 branch had no silencing path at all (only 403/404 did, and only for a fixed set of known-optional URLs), so any backend hiccup there spammed "Internal Server Error" toasts on top of an otherwise-working page. Added a SILENT_HTTP_ERROR HttpContextToken so a caller can opt a specific request out of the toast for every status code (not just 403/404) when it already handles failure itself — threaded as an optional `silent` param through ClientsService's searchByText/getClientData/getClientKyc/ getClientAddressData (default false, so no other of their ~50+ call sites change behavior) and set from ClientsComponent's list-page calls. The risk-rules endpoint was simpler: just added to the interceptor's existing URL-based silent-request list alongside client images/KYC/pending-edits.
This commit is contained in:
+10
-2
@@ -548,8 +548,16 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
|
|||||||
this.maxDate = this.settingsService.businessDate;
|
this.maxDate = this.settingsService.businessDate;
|
||||||
this.setOptions();
|
this.setOptions();
|
||||||
|
|
||||||
this.clientService.getClientRiskRules().subscribe((rules) => {
|
// Risk rules are an enhancement on top of the default low/medium-risk fallback in
|
||||||
this.riskRules = rules || [];
|
// calculatedRisk — if the endpoint isn't available (e.g. not yet deployed on this
|
||||||
|
// backend), fail silently rather than surfacing a raw HTTP-error toast to the user.
|
||||||
|
this.clientService.getClientRiskRules().subscribe({
|
||||||
|
next: (rules) => {
|
||||||
|
this.riskRules = rules || [];
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.riskRules = [];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
// Fetch marital status options from code values
|
// Fetch marital status options from code values
|
||||||
this.clientService.getCodeValuesByName('MaritalStatus').subscribe((options) => {
|
this.clientService.getCodeValuesByName('MaritalStatus').subscribe((options) => {
|
||||||
|
|||||||
@@ -137,7 +137,16 @@ export class ClientsComponent implements OnInit {
|
|||||||
private getClients() {
|
private getClients() {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.clientService
|
this.clientService
|
||||||
.searchByText(this.filterText, this.currentPage, this.pageSize, this.sortAttribute, this.sortDirection)
|
.searchByText(
|
||||||
|
this.filterText,
|
||||||
|
this.currentPage,
|
||||||
|
this.pageSize,
|
||||||
|
this.sortAttribute,
|
||||||
|
this.sortDirection,
|
||||||
|
false,
|
||||||
|
'',
|
||||||
|
true
|
||||||
|
)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
(data: any) => {
|
(data: any) => {
|
||||||
this.allRows = data.content || [];
|
this.allRows = data.content || [];
|
||||||
@@ -166,7 +175,7 @@ export class ClientsComponent implements OnInit {
|
|||||||
// Use individual subscriptions instead of forkJoin to avoid EMPTY observable issues
|
// Use individual subscriptions instead of forkJoin to avoid EMPTY observable issues
|
||||||
clients.forEach((client, i) => {
|
clients.forEach((client, i) => {
|
||||||
this.clientService
|
this.clientService
|
||||||
.getClientData(client.id?.toString())
|
.getClientData(client.id?.toString(), true)
|
||||||
.pipe(catchError(() => of(null)))
|
.pipe(catchError(() => of(null)))
|
||||||
.subscribe((data: any) => {
|
.subscribe((data: any) => {
|
||||||
if (data?.legalForm) {
|
if (data?.legalForm) {
|
||||||
@@ -176,7 +185,7 @@ export class ClientsComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.clientService
|
this.clientService
|
||||||
.getClientKyc(client.id?.toString())
|
.getClientKyc(client.id?.toString(), true)
|
||||||
.pipe(catchError(() => of(null)))
|
.pipe(catchError(() => of(null)))
|
||||||
.subscribe((kyc: any) => {
|
.subscribe((kyc: any) => {
|
||||||
if (kyc) {
|
if (kyc) {
|
||||||
@@ -188,7 +197,7 @@ export class ClientsComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.clientService
|
this.clientService
|
||||||
.getClientAddressData(client.id?.toString())
|
.getClientAddressData(client.id?.toString(), true)
|
||||||
.pipe(catchError(() => of(null)))
|
.pipe(catchError(() => of(null)))
|
||||||
.subscribe((addr: any) => {
|
.subscribe((addr: any) => {
|
||||||
if (addr && Array.isArray(addr) && addr.length > 0) {
|
if (addr && Array.isArray(addr) && addr.length > 0) {
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
|
|
||||||
/** Angular Imports */
|
/** Angular Imports */
|
||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { HttpClient, HttpParams, HttpBackend, HttpHeaders } from '@angular/common/http';
|
import { HttpClient, HttpParams, HttpBackend, HttpHeaders, HttpContext } from '@angular/common/http';
|
||||||
|
import { SILENT_HTTP_ERROR } from 'app/core/http/silent-error.token';
|
||||||
|
|
||||||
/** rxjs Imports */
|
/** rxjs Imports */
|
||||||
import { Observable, of, throwError } from 'rxjs';
|
import { Observable, of, throwError } from 'rxjs';
|
||||||
@@ -66,8 +67,11 @@ export class ClientsService {
|
|||||||
return this.http.get(`/clients/template?officeId=${officeId}&staffInSelectedOfficeOnly=true`);
|
return this.http.get(`/clients/template?officeId=${officeId}&staffInSelectedOfficeOnly=true`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getClientData(clientId: string) {
|
getClientData(clientId: string, silent = false) {
|
||||||
return this.http.get(`/clients/${clientId}`);
|
return this.http.get(
|
||||||
|
`/clients/${clientId}`,
|
||||||
|
silent ? { context: new HttpContext().set(SILENT_HTTP_ERROR, true) } : {}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
createClient(client: any) {
|
createClient(client: any) {
|
||||||
@@ -153,8 +157,11 @@ export class ClientsService {
|
|||||||
return this.http.get(`/clients/${clientId}/accounts`);
|
return this.http.get(`/clients/${clientId}/accounts`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getClientAddressData(clientId: string) {
|
getClientAddressData(clientId: string, silent = false) {
|
||||||
return this.http.get(`/client/${clientId}/addresses`);
|
return this.http.get(
|
||||||
|
`/client/${clientId}/addresses`,
|
||||||
|
silent ? { context: new HttpContext().set(SILENT_HTTP_ERROR, true) } : {}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getClientChargesData(clientId: string) {
|
getClientChargesData(clientId: string) {
|
||||||
@@ -698,7 +705,8 @@ export class ClientsService {
|
|||||||
sortAttribute: string = '',
|
sortAttribute: string = '',
|
||||||
sortDirection: string = '',
|
sortDirection: string = '',
|
||||||
activeOnly: boolean = false,
|
activeOnly: boolean = false,
|
||||||
legalFormType: string = ''
|
legalFormType: string = '',
|
||||||
|
silent: boolean = false
|
||||||
) {
|
) {
|
||||||
const innerRequest: any = { text };
|
const innerRequest: any = { text };
|
||||||
if (activeOnly) {
|
if (activeOnly) {
|
||||||
@@ -727,7 +735,11 @@ export class ClientsService {
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return this.http.post(`/v2/clients/search`, request);
|
return this.http.post(
|
||||||
|
`/v2/clients/search`,
|
||||||
|
request,
|
||||||
|
silent ? { context: new HttpContext().set(SILENT_HTTP_ERROR, true) } : {}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -764,8 +776,11 @@ export class ClientsService {
|
|||||||
* Gets KYC details for a client.
|
* Gets KYC details for a client.
|
||||||
* @param clientId Client ID
|
* @param clientId Client ID
|
||||||
*/
|
*/
|
||||||
getClientKyc(clientId: string): Observable<any> {
|
getClientKyc(clientId: string, silent = false): Observable<any> {
|
||||||
return this.http.get(`/clients/${clientId}/kyc`);
|
return this.http.get(
|
||||||
|
`/clients/${clientId}/kyc`,
|
||||||
|
silent ? { context: new HttpContext().set(SILENT_HTTP_ERROR, true) } : {}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { environment } from '../../../environments/environment';
|
|||||||
import { Logger } from '../logger/logger.service';
|
import { Logger } from '../logger/logger.service';
|
||||||
import { AlertService } from '../alert/alert.service';
|
import { AlertService } from '../alert/alert.service';
|
||||||
import { TranslateService } from '@ngx-translate/core'; // Added import for TranslateService
|
import { TranslateService } from '@ngx-translate/core'; // Added import for TranslateService
|
||||||
|
import { SILENT_HTTP_ERROR } from './silent-error.token';
|
||||||
|
|
||||||
/** Initialize Logger */
|
/** Initialize Logger */
|
||||||
const log = new Logger('ErrorHandlerInterceptor');
|
const log = new Logger('ErrorHandlerInterceptor');
|
||||||
@@ -57,13 +58,30 @@ export class ErrorHandlerInterceptor implements HttpInterceptor {
|
|||||||
// "No pending edit for this client" is the normal/expected state — checked on every edit-page
|
// "No pending edit for this client" is the normal/expected state — checked on every edit-page
|
||||||
// load, not a real error the user needs to see.
|
// load, not a real error the user needs to see.
|
||||||
const isClientPendingEditRequest = request.url.match(/\/clients\/\d+\/edit\/pending$/);
|
const isClientPendingEditRequest = request.url.match(/\/clients\/\d+\/edit\/pending$/);
|
||||||
|
// Risk rules are an enhancement on top of a default risk-category fallback — if this
|
||||||
|
// endpoint isn't available on a given backend deployment, the client-general-step
|
||||||
|
// component already falls back to riskRules = [] silently; no toast needed here either.
|
||||||
|
const isKycRiskRulesRequest = request.url.includes('/kyc/risk-rules');
|
||||||
const isSilentRequest =
|
const isSilentRequest =
|
||||||
isClientImageRequest || isClientKycRequest || isDirectorRequest || isClientPendingEditRequest;
|
isClientImageRequest ||
|
||||||
|
isClientKycRequest ||
|
||||||
|
isDirectorRequest ||
|
||||||
|
isClientPendingEditRequest ||
|
||||||
|
isKycRiskRulesRequest;
|
||||||
|
|
||||||
if (!environment.production && !isSilentRequest) {
|
// Explicit per-request opt-out (e.g. per-row enrichment fetches on a list page whose
|
||||||
|
// component already recovers via its own catchError) — silenced for every status, since
|
||||||
|
// the caller took responsibility for handling failure when it set this.
|
||||||
|
const isSilentOverride = request.context.get(SILENT_HTTP_ERROR);
|
||||||
|
|
||||||
|
if (!environment.production && !isSilentRequest && !isSilentOverride) {
|
||||||
log.error(`Request Error: ${errorMessage}`);
|
log.error(`Request Error: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSilentOverride) {
|
||||||
|
throw response;
|
||||||
|
}
|
||||||
|
|
||||||
// Silently handle expected 403/404 errors (no image, no KYC record)
|
// Silently handle expected 403/404 errors (no image, no KYC record)
|
||||||
// Throw instead of EMPTY so component-level catchError can handle it
|
// Throw instead of EMPTY so component-level catchError can handle it
|
||||||
if (isSilentRequest && (status === 403 || status === 404)) {
|
if (isSilentRequest && (status === 403 || status === 404)) {
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Copyright since 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { HttpContextToken } from '@angular/common/http';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-request opt-out of ErrorHandlerInterceptor's toast. Set via
|
||||||
|
* `{ context: new HttpContext().set(SILENT_HTTP_ERROR, true) }` on calls whose caller already
|
||||||
|
* handles failures itself (e.g. per-row enrichment fetches on a list page) — the interceptor
|
||||||
|
* still throws so the caller's own catchError/error callback runs, it just skips the alert.
|
||||||
|
*/
|
||||||
|
export const SILENT_HTTP_ERROR = new HttpContextToken<boolean>(() => false);
|
||||||
Reference in New Issue
Block a user