From 753d23bbbb2863e869100fdd497ba05e545bd0ba Mon Sep 17 00:00:00 2001 From: phibvcfc Date: Mon, 13 Jul 2026 09:21:07 +0700 Subject: [PATCH 1/2] fix(sdk/react-native): prevent duplicate requests by flagging fetch-triggered XHR calls --- .../devconnect-react-native/src/client.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/client_sdks/devconnect-react-native/src/client.ts b/client_sdks/devconnect-react-native/src/client.ts index a44fdff..cd0f351 100644 --- a/client_sdks/devconnect-react-native/src/client.ts +++ b/client_sdks/devconnect-react-native/src/client.ts @@ -490,6 +490,7 @@ export class DevConnect { * own duplicate report if the count is > 0. */ private fetchInFlight: Map = new Map(); + private isInsideFetch = false; private constructor(config: DevConnectConfig & { resolvedHost: string }) { this.config = { @@ -920,8 +921,16 @@ export class DevConnect { dc.fetchInFlight.set(fetchKey, (dc.fetchInFlight.get(fetchKey) ?? 0) + 1); dc.send('client:network:request_start', { requestId, method, url, startTime, requestHeaders: reqHeaders, requestBody, source, via: 'fetch' }); + let responsePromise: Promise; + dc.isInsideFetch = true; try { - const response = await originalFetch(input, init); + responsePromise = originalFetch(input, init); + } finally { + dc.isInsideFetch = false; + } + + try { + const response = await responsePromise; const clone = response.clone(); let responseBody: any; try { const text = await clone.text(); try { responseBody = JSON.parse(text); } catch (_) { responseBody = text; } } catch (_) {} @@ -968,6 +977,7 @@ export class DevConnect { function PatchedXHR(this: any) { const xhr = new OriginalXHR(); + const isFetchXhr = dc.isInsideFetch; const requestId = generateId(); let method = 'GET', url = '', startTime = 0; const reqHeaders: Record = {}; @@ -1006,7 +1016,7 @@ export class DevConnect { // Skip the start report if the fetch interceptor already // covers this call (see handleLoadEnd for the matching skip). const xhrKey = `${method}\0${url}`; - const isFetchRequest = (dc.fetchInFlight.get(xhrKey) ?? 0) > 0; + const isFetchRequest = isFetchXhr || (dc.fetchInFlight.get(xhrKey) ?? 0) > 0; if (!isFetchRequest) { dc.send('client:network:request_start', { requestId, method, url, startTime, requestHeaders: reqHeaders, requestBody, source: classifyUrl(url), via: 'xhr' }); } @@ -1021,7 +1031,7 @@ export class DevConnect { // XHR path) with different requestIds, which the server // cannot merge downstream. const xhrKey = `${method}\0${url}`; - const isFetchRequest = (dc.fetchInFlight.get(xhrKey) ?? 0) > 0; + const isFetchRequest = isFetchXhr || (dc.fetchInFlight.get(xhrKey) ?? 0) > 0; if (isFetchRequest) { return; } From 15cbfa2a9dd66bf5e3e43ad3e6acf5381c308f5d Mon Sep 17 00:00:00 2001 From: phibvcfc Date: Mon, 13 Jul 2026 12:10:42 +0700 Subject: [PATCH 2/2] refactor: replace boolean isInsideFetch flag with fetchStackCount counter for tracking nested requests --- .../devconnect-react-native/src/client.ts | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/client_sdks/devconnect-react-native/src/client.ts b/client_sdks/devconnect-react-native/src/client.ts index cd0f351..4e2b160 100644 --- a/client_sdks/devconnect-react-native/src/client.ts +++ b/client_sdks/devconnect-react-native/src/client.ts @@ -490,7 +490,7 @@ export class DevConnect { * own duplicate report if the count is > 0. */ private fetchInFlight: Map = new Map(); - private isInsideFetch = false; + private fetchStackCount = 0; private constructor(config: DevConnectConfig & { resolvedHost: string }) { this.config = { @@ -922,11 +922,33 @@ export class DevConnect { dc.send('client:network:request_start', { requestId, method, url, startTime, requestHeaders: reqHeaders, requestBody, source, via: 'fetch' }); let responsePromise: Promise; - dc.isInsideFetch = true; + dc.fetchStackCount++; try { responsePromise = originalFetch(input, init); + } catch (err: any) { + dc.send('client:network:request_complete', { + requestId, + method, + url, + statusCode: 0, + startTime, + endTime: Date.now(), + duration: Date.now() - startTime, + requestHeaders: reqHeaders, + requestBody, + error: err?.message ?? String(err), + source, + via: 'fetch', + }); + const currentCount = dc.fetchInFlight.get(fetchKey) ?? 0; + if (currentCount <= 1) { + dc.fetchInFlight.delete(fetchKey); + } else { + dc.fetchInFlight.set(fetchKey, currentCount - 1); + } + throw err; } finally { - dc.isInsideFetch = false; + dc.fetchStackCount--; } try { @@ -977,7 +999,7 @@ export class DevConnect { function PatchedXHR(this: any) { const xhr = new OriginalXHR(); - const isFetchXhr = dc.isInsideFetch; + const isFetchXhr = dc.fetchStackCount > 0; const requestId = generateId(); let method = 'GET', url = '', startTime = 0; const reqHeaders: Record = {};