Skip to content

fix(dio_web_adapter): stop timeout timers once the request has ended - #2626

Open
wassafr wants to merge 1 commit into
cfug:mainfrom
wassafr:fix/web-adapter-timers-after-request
Open

wassafr wants to merge 1 commit into
cfug:mainfrom
wassafr:fix/web-adapter-timers-after-request

Conversation

@wassafr

@wassafr wassafr commented Oct 26, 2026

Copy link
Copy Markdown

BrowserHttpClientAdapter arms a receive timeout timer on every XHR progress event, and relied on the onDone callback of xhr.onProgress to cancel it. DOM event streams never close, so onDone is never called: after a request completes, its receive timer stays armed until it fires, receiveTimeout later. The connect timeout timer of a request is likewise left armed when no progress event cancels it (e.g. when the request fails early).

While armed, the timer closure keeps the XHR and its response reachable, so every response stays in memory for up to receiveTimeout after it has been delivered. We hit this in a Flutter web app that syncs a ~300 MB catalogue in pages of ~10 MB with receiveTimeout: 30s: heap snapshots showed all pages still retained once written to the local database (~1 GB in the tab), held by BrowserHttpClientAdapter_fetch_watchReceiveTimeout_closure. Downloading many media files showed the same pattern.

This PR stops both timers on the XHR loadend event, which is dispatched once a request has ended, whatever the outcome (load, error, abort or timeout). It is dispatched after load, error, abort and timeout, so the existing handlers of those events run first and timeout classification is unchanged. The onDone callback, which never runs, is removed.

New Pull Request Checklist

  • I have read the Documentation
  • I have read the Agent Contribution Guidelines (required if any part of the change was produced with AI assistance)
  • I have searched for a similar pull request in the project and found none
  • I have updated this branch with the latest main branch to avoid conflicts (via merge from master or rebase)
  • I have added the required tests to prove the fix/feature I'm adding
  • I have updated the documentation (if necessary) (not applicable — no API change)
  • I have run the tests without failures
  • I have updated the CHANGELOG.md in the corresponding package

Additional context and info (if any)

Verification: the new browser test plugins/web_adapter/test/timers_test.dart records the timers the adapter arms for a request (through a zone) and checks that none is still active once the response has been delivered. It fails on main (one active timer left) and passes with this change. The whole plugins/web_adapter suite passes in Chrome (dart test -p chrome, 37 tests), and dart analyze --fatal-infos / dart format are clean.

The dio package web tests in Chrome, which run the shared dio_test suites (timeouts, cancellation, downloads…) on this adapter, give the same result with and without this change: 193 passed, 1 skipped, 1 failed with an XHR connection error from the remote test server, identical on main.

Unverified: Firefox (not available on my machine); loadend is part of the XHR standard and supported by all browsers dio targets.

Timeout handling is a sensitive area per AGENTS.md: cc @cfug/dio-core.

Root-cause analysis (heap snapshots), implementation and test by Claude (Claude Code).

The receive timeout timer, re-armed on every XHR `progress` event, was
meant to be cancelled by the `onDone` callback of `xhr.onProgress`, but
DOM event streams never close: the timer stayed armed until it fired,
`receiveTimeout` after the response. The connect timeout timer could
likewise outlive its request. While armed, a timer keeps the XHR and its
response reachable, so each response stayed in memory for up to the
configured timeout.

Stop both timers on `loadend`, dispatched once the request has ended
(load, error, abort or timeout) and after the handlers of those events.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@wassafr
wassafr requested a review from a team as a code owner October 26, 2026 23:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant