Description
Description
When XMLHttpRequest.abort() runs before iOS has returned the native request ID, the eventual creation callback still assigns that ID to the XHR. The native request is never cancelled. If the same XHR is reused, this callback can overwrite the replacement request's ID: the replacement response is then ignored, or the aborted request's response is delivered as the replacement's.
This can occur when cancelling and retrying an upload. iOS builds a URI/FormData body asynchronously before creating the native request. A replacement GET can therefore receive its ID before the aborted upload.
Steps to reproduce
A deterministic regression using the existing XHR Jest harness:
- Defer RCTNetworking.sendRequest's creation callbacks.
- Open/send request A, then abort it before delivering A's ID.
- Open/send request B on the same XHR.
- Deliver B's creation callback with ID 200, then A's callback with ID 100.
- Deliver B's response and completion events for ID 200.
Actual: A is never passed to abortRequest; A's callback replaces the active ID with 100; B's events are ignored. The new regression tests also cover the opposite callback order and verify that stale responses are ignored.
Expected: cancel A when its ID becomes available; retain B's ID and accept B's response.
The linked RNTester Playground reproducer uses real networking: a POST body URI takes two seconds to load, and the replacement GET takes four seconds to return. Start its Node server, run RNTester on an iOS simulator, open Playground and press Reproduce. See the linked instructions for setup and expected output. This native Playground is supplied for verification; it has not been run on a simulator in this environment.
React Native Version
v0.88.1 and main at 25d63d4. The latest stable tag's XMLHttpRequest.js was run in the monorepo's Jest harness: the same three race tests fail on both versions. This is source-level validation, not a full v0.88.1 application build.
Affected Platforms
Runtime - iOS
Output of npx @react-native-community/cli info
info Fetching system and libraries information...
System:
OS: macOS 27.2
CPU: (12) arm64 Apple M4 Pro
Memory: 4.28 GB / 48.00 GB
Shell:
version: "5.9"
path: /bin/zsh
Binaries:
Node:
version: 22.23.4
path: ~/.nvm/versions/node/v22.23.4/bin/node
Yarn: Not Found
npm:
version: 10.9.9
path: ~/.nvm/versions/node/v22.23.4/bin/npm
Watchman: Not Found
Managers:
CocoaPods: Not Found
SDKs:
iOS SDK:
Platforms:
- DriverKit 27.2
- iOS 27.2
- macOS 27.2
- tvOS 27.2
- visionOS 27.2
- watchOS 27.2
Android SDK: Not Found
IDEs:
Android Studio: Not Found
Xcode:
version: 27.2/27B52
path: /usr/bin/xcodebuild
Languages:
Java:
version: 17.0.20.1
path: /usr/bin/javac
Ruby:
version: 2.6.10
path: /usr/bin/ruby
npmPackages:
"@react-native-community/cli": Not Found
react:
installed: 19.3.0
wanted: 19.3.0
react-native: Not Found
react-native-macos: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: Not found
newArchEnabled: Not found
iOS:
hermesEnabled: Not found
newArchEnabled: false
Stacktrace or Logs
Unpatched main and stable-source runs:
Tests: 3 failed, 15 passed, 18 total
Expected abortRequest calls: 1
Received abortRequest calls: 0
Direct state observation after B(200), A(100): the active request ID changes from 200 to 100. B then remains OPENED when its completion is delivered.
After the proposed fix:
Test Suites: 5 passed, 5 total
Tests: 59 passed, 59 total
MANDATORY Reproducer
Additional context
The creation callback in XMLHttpRequest.send() captures the XHR object but no request identity. The iOS send path dispatches work asynchronously and waits for buildRequest completion before allocating its ID (RCTNetworking.mm). A per-request generation captured by send(), invalidated by reset(), prevents the obsolete callback from attaching to a reused XHR and permits cancelling the late native ID.
This does not cancel body construction before an ID exists; it cancels the native request as soon as that ID is delivered. I found no existing issue or PR covering this callback race in searches for XMLHttpRequest abort, requestId abort, and stale request callbacks.
Activity