Skip to content

fix(xhr): flush final progress during the live loadend dispatch - #11121

Merged
jasonsaayman merged 5 commits into
axios:v1.xfrom
ostapondo:fix/stream-chunk-throttle
Aug 13, 2026
Merged

jasonsaayman merged 5 commits into
axios:v1.xfrom
ostapondo:fix/stream-chunk-throttle

Conversation

@ostapondo

@ostapondo ostapondo commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Axios 1.7.0 introduced throttled progress delivery. Timer-deferred XHR progress callbacks therefore receive an event whose DOM dispatch has already ended, so event.currentTarget is null. Consumers that incrementally read responseText through currentTarget can miss every deferred chunk, reproducing #6796 most visibly in Firefox and Safari.

This change force-delivers one final onDownloadProgress callback from the still-dispatching successful XHR loadend event. That gives cumulative readers a live target and the complete response while preserving the established behavior of upload, stream-error, abort-reason, and failed-download flushes. XHR implementations without onloadend instead replay their pending progress event through the legacy ready-state fallback.

The forced delivery path is explicit rather than inferred from a flush argument's shape. A final callback can also throw or synchronously cancel the request without leaving settlement pending or causing the XHR handler to dereference a cleared request.

Playwright verification covers Chromium, Firefox, and WebKit. A currentTarget-based reader that previously received only the initial part of a fast stream now recovers the complete payload.

Linked issue

Closes #6796

Changes

  • lib/helpers/throttle.js: keeps ordinary flush argument-agnostic and adds an explicit flushWith function for replacement arguments.
  • lib/helpers/progressEventReducer.js: exposes that explicit forced-delivery function without inspecting stream errors or cancellation reasons.
  • lib/adapters/xhr.js: force-delivers only the successful final download event, replays pending progress in the eventless ready-state fallback, stops cleanly after synchronous cancellation, and preserves prior failure/upload flush behavior.
  • tests/browser/progress.browser.test.js: uses real EventTarget/ProgressEvent semantics and adds eight browser regression tests covering live final delivery, complete streamed reads, the ready-state fallback, cancellation, listener exceptions, status-zero failures, upload failures, and deferred event.target behavior.
  • tests/unit/helpers/progressEventReducer.test.js and tests/unit/helpers/throttle.test.js: cover explicit forced delivery, pending-event replay, loaded-shaped errors, throwing getters, and the throttle tuple contract.
  • PRE_RELEASE_CHANGELOG.md and PRE_RELEASE_DOCS.md: record the download-only guarantee and retained failure semantics.

Testing

  • npx eslint lib/adapters/xhr.js lib/helpers/progressEventReducer.js lib/helpers/throttle.js
  • npm run test:vitest:unit — 58 files, 1,045 tests passed
  • npm run test:vitest:browser:headless -- tests/browser/progress.browser.test.js — Chromium, Firefox, and WebKit; 42 tests passed

Checklist

  • Tests added or updated
  • Deferred documentation recorded in PRE_RELEASE_DOCS.md; no public type change
  • No breaking API change

🏄

@ostapondo
ostapondo requested a review from jasonsaayman as a code owner August 3, 2026 11:01

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 7 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread lib/adapters/xhr.js Outdated
@greptile-apps

greptile-apps Bot commented Aug 3, 2026 •

Copy link
Copy Markdown

Greptile Summary

This change delivers the final XHR download progress callback while the loadend event still has a live target, retains pending progress for the legacy ready-state completion path, and ensures synchronous cancellation or listener exceptions do not leave a request unsettled.

The focused XHR repro showed the previous revision lost the final loaded=8 fallback update, while this revision delivers it. The same repro confirmed live-target delivery, cancellation rejection, and settlement after a throwing final listener. Browser progress coverage passed 14/14 tests in headless Chromium, and adjacent XHR adapter coverage passed 7/7 tests.

Confidence Score: 5/5

The PR is safe to merge; no blocking failure remains.

The focused browser and XHR adapter checks passed, and the exercised completion, fallback, cancellation, and listener-exception paths behaved as intended.

T-Rex T-Rex Logs

What T-Rex did

  • Ran the authored EventTarget-based XHR repro against the parent revision 81f0b90 and observed the ready-state fallback delivered only loaded=4.
  • Re-ran the same repro at the PR tip and confirmed live loadend delivery, fallback replay, synchronous abort rejection, and settlement after a throwing final listener all passed.
  • Executed the focused browser progress suite in headless Chromium, which passed all 14 tests.
  • Ran the adjacent XHR adapter unit suite, which passed all 7 tests.
  • Uploaded a narrow repro script and confirmed that no production files were modified by the change set.

View all artifacts

T-Rex Ran code and verified through T-Rex

Reviews (6): Last reviewed commit: "fix(xhr): preserve fallback download pro..." | Re-trigger Greptile

Comment thread lib/adapters/xhr.js Outdated
@jasonsaayman jasonsaayman added the commit::fix The PR is related to a bugfix label Aug 4, 2026
@ostapondo
ostapondo force-pushed the fix/stream-chunk-throttle branch 2 times, most recently from c80b069 to b54e649 Compare August 4, 2026 23:01
Throttled progress deliveries replay events whose dispatch has already
finished, so event.currentTarget is null and listeners reading incremental
data from it lose every chunk delivered through the throttle timer or the
loadend flush. Flush with the still-dispatching loadend event instead, so a
final delivery with the complete transfer state always reaches the listener
while the event has a live target.

The reducer only treats a flush argument with a numeric loaded field as a
fresh event; stream errors and abort reasons passed by other adapters keep
replaying the last pending event.
The live loadend flush runs user code before settle(), so a throwing
onDownloadProgress left the promise pending and cancellation listeners
attached. Guard the flush and rethrow asynchronously, matching how
listener errors already surface on the throttle timer path.
@ostapondo
ostapondo force-pushed the fix/stream-chunk-throttle branch from b54e649 to 693d477 Compare August 9, 2026 10:24

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 8 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread lib/adapters/xhr.js Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

commit::fix The PR is related to a bugfix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Axios version >= 1.7.0 Streaming Chunk Parsing Issue in Safari and Firefox

2 participants