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(); + }); });