From 25b44955a2242972500d199cb9e180111b98f738 Mon Sep 17 00:00:00 2001 From: Darren Li Date: Tue, 23 Jun 2026 16:41:41 +1000 Subject: [PATCH] fix(reporting): don't beacon transport/network-failure error reports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ErrorReportingService.report() beacons every report to /v1/errors. When a report merely describes a failed network request — e.g. an identity request that failed with "Failed to fetch" (also how a rate-limited 429 surfaces when its response is CORS-blocked or dropped) — beaconing it emits another request that is itself rate-limited/blocked, feeding back into the same per-IP limit. Drop reports whose message matches a browser network-failure phrase ("Failed to fetch" / "Load failed" / Firefox / "Network request failed"). Genuine application errors (other messages) are still reported. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/Rokt-Kit.ts | 23 +++++++++++++++++++++++ test/src/tests.spec.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/src/Rokt-Kit.ts b/src/Rokt-Kit.ts index 5f91127..71c3634 100644 --- a/src/Rokt-Kit.ts +++ b/src/Rokt-Kit.ts @@ -515,6 +515,23 @@ function _getUserAgent(): string | undefined { return typeof window !== 'undefined' ? window.navigator?.userAgent : undefined; } +// Browser network-failure phrases (Chromium / WebKit / Firefox) raised when a request +// never produces a readable response — including a rate-limited 429 whose response is +// CORS-blocked or dropped. Reports describing such a failure must not be beaconed: doing +// so emits another request that is itself rate-limited/blocked, feeding the same limit. +const NETWORK_FAILURE_PATTERNS = [ + 'failed to fetch', + 'load failed', + 'networkerror when attempting to fetch resource', + 'network request failed', +]; + +function _isNetworkFailureMessage(message: string | undefined): boolean { + if (!message) return false; + const normalized = message.toLowerCase(); + return NETWORK_FAILURE_PATTERNS.some((pattern) => normalized.includes(pattern)); +} + class RateLimiter { private _logCount: Record = {}; @@ -622,6 +639,12 @@ class ErrorReportingService { report(error: ErrorReport | null | undefined): void { if (!error) return; + // Drop reports that merely describe a transport/network failure of a prior request + // (e.g. an identity request that failed with "Failed to fetch", which is also how a + // rate-limited 429 surfaces). Beaconing these to /v1/errors emits another request + // that is itself rate-limited/blocked, feeding back into the same limit. Genuine + // application errors (different messages) are still reported. + if (_isNetworkFailureMessage(error.message)) return; const severity = error.severity || WSDKErrorSeverity.ERROR; this._transport.send(this._errorUrl, severity, error.message, error.code, error.stackTrace); } diff --git a/test/src/tests.spec.ts b/test/src/tests.spec.ts index 8fc54b8..a7ee44a 100644 --- a/test/src/tests.spec.ts +++ b/test/src/tests.spec.ts @@ -6144,6 +6144,34 @@ describe('Rokt Forwarder', () => { expect(body.reporter).toBe('mp-wsdk'); }); + it.each([ + 'Error sending identity request to servers - Failed to fetch', + 'Load failed', + 'NetworkError when attempting to fetch resource.', + ])('should NOT beacon a transport/network-failure report: "%s" (no feedback loop)', (message) => { + const service = new ErrorReportingServiceClass( + { errorUrl: 'test.com/v1/errors', isLoggingEnabled: true }, + '1.0.0', + 'test-guid', + ); + service.report({ message, code: ErrorCodesConst.UNKNOWN_ERROR, severity: WSDKErrorSeverityConst.ERROR }); + expect(fetchCalls.length).toBe(0); + }); + + it('should still beacon a genuine application error (not a network failure)', () => { + const service = new ErrorReportingServiceClass( + { errorUrl: 'test.com/v1/errors', isLoggingEnabled: true }, + '1.0.0', + 'test-guid', + ); + service.report({ + message: 'Error sending identity request to servers - e.split is not a function', + code: ErrorCodesConst.UNKNOWN_ERROR, + severity: WSDKErrorSeverityConst.ERROR, + }); + expect(fetchCalls.length).toBe(1); + }); + it('should send warning reports to the errors endpoint', () => { const service = new ErrorReportingServiceClass( { errorUrl: 'test.com/v1/errors', isLoggingEnabled: true },