From f687e1808d7477ba21b95fedec3019241b37bad5 Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Thu, 30 Jul 2026 13:41:28 +0530 Subject: [PATCH] fix(http): stop surfacing background/enrichment-fetch failures as error toasts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../client-general-step.component.ts | 12 +++++-- src/app/clients/clients.component.ts | 17 +++++++--- src/app/clients/clients.service.ts | 33 ++++++++++++++----- .../core/http/error-handler.interceptor.ts | 22 +++++++++++-- src/app/core/http/silent-error.token.ts | 17 ++++++++++ 5 files changed, 84 insertions(+), 17 deletions(-) create mode 100644 src/app/core/http/silent-error.token.ts diff --git a/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts b/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts index dba14f5e8..78d6a0728 100644 --- a/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts +++ b/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts @@ -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) => { diff --git a/src/app/clients/clients.component.ts b/src/app/clients/clients.component.ts index fd99d30f7..98f9df007 100644 --- a/src/app/clients/clients.component.ts +++ b/src/app/clients/clients.component.ts @@ -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) { diff --git a/src/app/clients/clients.service.ts b/src/app/clients/clients.service.ts index b412ccc45..cbb893260 100644 --- a/src/app/clients/clients.service.ts +++ b/src/app/clients/clients.service.ts @@ -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 { - return this.http.get(`/clients/${clientId}/kyc`); + getClientKyc(clientId: string, silent = false): Observable { + return this.http.get( + `/clients/${clientId}/kyc`, + silent ? { context: new HttpContext().set(SILENT_HTTP_ERROR, true) } : {} + ); } /** diff --git a/src/app/core/http/error-handler.interceptor.ts b/src/app/core/http/error-handler.interceptor.ts index d83994a33..0d9413845 100644 --- a/src/app/core/http/error-handler.interceptor.ts +++ b/src/app/core/http/error-handler.interceptor.ts @@ -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)) { diff --git a/src/app/core/http/silent-error.token.ts b/src/app/core/http/silent-error.token.ts new file mode 100644 index 000000000..9623b800e --- /dev/null +++ b/src/app/core/http/silent-error.token.ts @@ -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(() => false);