Skip to content

XMLHttpRequest abort before iOS request creation can corrupt a reused request #59805

Description

@shoutoutuoadi325

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:

  1. Defer RCTNetworking.sendRequest's creation callbacks.
  2. Open/send request A, then abort it before delivering A's ID.
  3. Open/send request B on the same XHR.
  4. Deliver B's creation callback with ID 200, then A's callback with ID 100.
  5. 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

RNTesterPlayground.js

Server and run instructions

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions