fix(http): stop surfacing background/enrichment-fetch failures as error toasts
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s

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:
2026-07-30 13:41:28 +05:30
parent 186a9057a1
commit f687e1808d
5 changed files with 84 additions and 17 deletions
@@ -548,8 +548,16 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
this.maxDate = this.settingsService.businessDate;
this.setOptions();
this.clientService.getClientRiskRules().subscribe((rules) => {
this.riskRules = rules || [];
// Risk rules are an enhancement on top of the default low/medium-risk fallback in
// 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
this.clientService.getCodeValuesByName('MaritalStatus').subscribe((options) => {
+13 -4
View File
@@ -137,7 +137,16 @@ export class ClientsComponent implements OnInit {
private getClients() {
this.isLoading = true;
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(
(data: any) => {
this.allRows = data.content || [];
@@ -166,7 +175,7 @@ export class ClientsComponent implements OnInit {
// Use individual subscriptions instead of forkJoin to avoid EMPTY observable issues
clients.forEach((client, i) => {
this.clientService
.getClientData(client.id?.toString())
.getClientData(client.id?.toString(), true)
.pipe(catchError(() => of(null)))
.subscribe((data: any) => {
if (data?.legalForm) {
@@ -176,7 +185,7 @@ export class ClientsComponent implements OnInit {
});
this.clientService
.getClientKyc(client.id?.toString())
.getClientKyc(client.id?.toString(), true)
.pipe(catchError(() => of(null)))
.subscribe((kyc: any) => {
if (kyc) {
@@ -188,7 +197,7 @@ export class ClientsComponent implements OnInit {
});
this.clientService
.getClientAddressData(client.id?.toString())
.getClientAddressData(client.id?.toString(), true)
.pipe(catchError(() => of(null)))
.subscribe((addr: any) => {
if (addr && Array.isArray(addr) && addr.length > 0) {
+24 -9
View File
@@ -8,7 +8,8 @@
/** Angular Imports */
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 */
import { Observable, of, throwError } from 'rxjs';
@@ -66,8 +67,11 @@ export class ClientsService {
return this.http.get(`/clients/template?officeId=${officeId}&staffInSelectedOfficeOnly=true`);
}
getClientData(clientId: string) {
return this.http.get(`/clients/${clientId}`);
getClientData(clientId: string, silent = false) {
return this.http.get(
`/clients/${clientId}`,
silent ? { context: new HttpContext().set(SILENT_HTTP_ERROR, true) } : {}
);
}
createClient(client: any) {
@@ -153,8 +157,11 @@ export class ClientsService {
return this.http.get(`/clients/${clientId}/accounts`);
}
getClientAddressData(clientId: string) {
return this.http.get(`/client/${clientId}/addresses`);
getClientAddressData(clientId: string, silent = false) {
return this.http.get(
`/client/${clientId}/addresses`,
silent ? { context: new HttpContext().set(SILENT_HTTP_ERROR, true) } : {}
);
}
getClientChargesData(clientId: string) {
@@ -698,7 +705,8 @@ export class ClientsService {
sortAttribute: string = '',
sortDirection: string = '',
activeOnly: boolean = false,
legalFormType: string = ''
legalFormType: string = '',
silent: boolean = false
) {
const innerRequest: any = { text };
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.
* @param clientId Client ID
*/
getClientKyc(clientId: string): Observable<any> {
return this.http.get(`/clients/${clientId}/kyc`);
getClientKyc(clientId: string, silent = false): Observable<any> {
return this.http.get(
`/clients/${clientId}/kyc`,
silent ? { context: new HttpContext().set(SILENT_HTTP_ERROR, true) } : {}
);
}
/**
+20 -2
View File
@@ -21,6 +21,7 @@ import { environment } from '../../../environments/environment';
import { Logger } from '../logger/logger.service';
import { AlertService } from '../alert/alert.service';
import { TranslateService } from '@ngx-translate/core'; // Added import for TranslateService
import { SILENT_HTTP_ERROR } from './silent-error.token';
/** Initialize Logger */
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
// load, not a real error the user needs to see.
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 =
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}`);
}
if (isSilentOverride) {
throw response;
}
// Silently handle expected 403/404 errors (no image, no KYC record)
// Throw instead of EMPTY so component-level catchError can handle it
if (isSilentRequest && (status === 403 || status === 404)) {
+17
View File
@@ -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);