From 074963e0c1e669828bcc86dc44b7f59acc1cd32e Mon Sep 17 00:00:00 2001 From: Bahadir10 Date: Sat, 7 Nov 2026 13:18:17 +0300 Subject: [PATCH] Fix XMLHttpRequest abort before native request creation corrupting reused requests Summary: **Problem**: When `XMLHttpRequest.abort()` runs before the native platform has returned the request ID, the subsequent creation callback still sets `this._requestId`. The native request is never cancelled. Furthermore, if the same XHR instance is reused (such as in an abort & retry pattern or connection pooling), this delayed callback overwrites the replacement request's ID and causes subsequent events to be misrouted or ignored. **Proposed change**: Introduce `_requestGeneration` counter on `XMLHttpRequest`. When `abort()` or `_reset()` occurs, the generation counter is incremented. If the creation callback returns for a superseded or aborted generation, the native request is immediately aborted via `RCTNetworking.abortRequest(requestId)` without updating `this._requestId`. Fixes #59805 Changelog: [Network][Fixed] - Prevent `XMLHttpRequest.abort()` before native request creation from corrupting reused requests and ensure native request is aborted --- .../Libraries/Network/XMLHttpRequest.js | 12 ++++++++++- .../Network/__tests__/XMLHttpRequest-test.js | 20 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/react-native/Libraries/Network/XMLHttpRequest.js b/packages/react-native/Libraries/Network/XMLHttpRequest.js index 9761aa02dd98..a9f6921fc604 100644 --- a/packages/react-native/Libraries/Network/XMLHttpRequest.js +++ b/packages/react-native/Libraries/Network/XMLHttpRequest.js @@ -176,6 +176,7 @@ class XMLHttpRequest extends EventTarget { upload: XMLHttpRequestEventTarget = new XMLHttpRequestEventTarget(); _requestId: ?number; + _requestGeneration: number = 0; _subscriptions: Array; _aborted: boolean = false; @@ -204,6 +205,7 @@ class XMLHttpRequest extends EventTarget { } _reset(): void { + this._requestGeneration++; this.readyState = this.UNSENT; this.responseHeaders = undefined; this.status = 0; @@ -652,6 +654,7 @@ class XMLHttpRequest extends EventTarget { 'XMLHttpRequest URL needs to be defined (%s).', friendlyName, ); + const currentGeneration = ++this._requestGeneration; RCTNetworking.sendRequest( method, this._trackingName ?? undefined, @@ -661,7 +664,13 @@ class XMLHttpRequest extends EventTarget { nativeResponseType, incrementalEvents, this.timeout, - (requestId: number) => this.__didCreateRequest(requestId), + (requestId: number) => { + if (currentGeneration !== this._requestGeneration || this._aborted) { + RCTNetworking.abortRequest(requestId); + return; + } + this.__didCreateRequest(requestId); + }, this.withCredentials, ); }; @@ -674,6 +683,7 @@ class XMLHttpRequest extends EventTarget { abort(): void { this._aborted = true; + this._requestGeneration++; if (this._requestId != null) { RCTNetworking.abortRequest(this._requestId); } diff --git a/packages/react-native/Libraries/Network/__tests__/XMLHttpRequest-test.js b/packages/react-native/Libraries/Network/__tests__/XMLHttpRequest-test.js index 70fa857e2a8e..1307d4992823 100644 --- a/packages/react-native/Libraries/Network/__tests__/XMLHttpRequest-test.js +++ b/packages/react-native/Libraries/Network/__tests__/XMLHttpRequest-test.js @@ -318,4 +318,24 @@ describe('XMLHttpRequest', function () { 'also-here: Mr. PB\r\newok: lego\r\nfoo-test: 1, 2\r\n__custom: token\r\n', ); }); + it('should abort late native request when aborted before requestId was delivered', function () { + const NativeModules = require('../../BatchedBridge/NativeModules').default; + const abortSpy = jest.spyOn(NativeModules.Networking, 'abortRequest'); + let capturedCallback; + jest.spyOn(NativeModules.Networking, 'sendRequest').mockImplementation((...args) => { + capturedCallback = args[8]; + }); + + xhr.open('GET', 'http://example.com'); + xhr.send(); + xhr.abort(); + + // Simulate late native request creation callback arriving after abort + expect(abortSpy).not.toHaveBeenCalled(); + capturedCallback(999); + expect(abortSpy).toHaveBeenCalledWith(999); + // Ensure this late request ID did not corrupt the XHR state + // $FlowFixMe[prop-missing] + expect(xhr._requestId).toBeNull(); + }); });