🐛 Fix QueuedInterceptor queue stalling when the active request is cancelled#2526
🐛 Fix QueuedInterceptor queue stalling when the active request is cancelled#2526ultramcu wants to merge 1 commit into
Conversation
|
Thanks for the PR! Requesting our agent to review... |
There was a problem hiding this comment.
Pull request overview
Fixes a hang in QueuedInterceptor where cancelling the currently active task (whose async onRequest/onResponse/onError callback had not yet called handler.next/resolve/reject) would never release the queue slot, stalling all subsequent requests routed through the same interceptor. The fix adds an idempotent queue-advance triggered both by handler completion and by cancellation of the active task.
Changes:
- Refactor
_handleQueueto run tasks via arunTaskclosure with an idempotentadvance()used both by_processNextInQueueand a per-task cancel hook registered only while the task is active. - Add a
_cancelTokenOfhelper that extracts theCancelTokenfrom request/response/error payloads. - Add two regression tests covering the request and response queue stall scenarios, plus a
CHANGELOG.mdentry.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| dio/lib/src/interceptor.dart | Restructures _handleQueue to advance the queue idempotently on either handler completion or cancellation of the active task. |
| dio/test/queued_interceptor_test.dart | Adds request-queue and response-queue regression tests for the cancellation-induced stall. |
| dio/CHANGELOG.md | Adds an Unreleased entry describing the fix. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
When a request is cancelled (via CancelToken) while it is the active task in a QueuedInterceptor's queue — its onRequest/onResponse/onError callback still running an async step and not yet having called next/resolve/reject — the queue stalled forever. The queue only advances from inside the handler (via _processNextInQueue), but cancellation completes the request future without completing the queued handler, so the active slot was never released and every subsequent request through the interceptor hung, including ones with no cancel token. Each task now arms a one-shot cancel hook (only while it is the active task) that releases the queue slot if the request is cancelled before the handler completes; queue advancement is made idempotent so a normal completion and a cancellation cannot double-advance. No public API change. Adds request-queue and response-queue regression tests.
ed004d6 to
1cb445f
Compare
|
Thanks for the review @AlexV525 + Copilot — both findings were valid and have been addressed in 1. Memory leak (interceptor.dart) — fixedThe Fix: a clearable nullable cell Verified empirically with a Finalizer-instrumented harness (5 tasks × 16 MB payload each, with a long-lived
The cancel-before-completion path still triggers 2.
|
Code Coverage Report: Only Changed Files listed
Minimum allowed coverage is |
Summary
When a request is cancelled (via
CancelToken) while it is the active task in aQueuedInterceptor's internal queue — i.e. itsonRequest/onResponse/onErrorcallback is still running an async step and has not yet calledhandler.next/resolve/reject— the queue stalls permanently. Every subsequent request routed through the sameQueuedInterceptor, including requests with no cancel token, then hangs forever.This hits a very common, real-world setup: a
QueuedInterceptorthat refreshes an auth token in an asynconRequest(the canonical reason to use a queued interceptor), where the user cancels the in-flight request.Root cause
QueuedInterceptor._handleQueueonly advances the queue from inside the handler'snext/resolve/reject(via_processNextInQueue). Cancellation completes the request future (throughlistenCancelForAsyncTask) without ever completing the queued handler, so the active task is never released,processingstaystrue, and no further task is dequeued.Fix
Each task now arms a one-shot cancel hook — registered only while it is the active task, so a queued task is never advanced out of turn — that releases the queue slot if the request is cancelled before the handler completes. Queue advancement is made idempotent (an
advancedguard) so a normal completion and a cancellation can't double-advance. No public API change; behaviour for non-cancelled requests is unchanged.Tests
Added two regression tests in
queued_interceptor_test.dart(request queue + response queue): they cancel the active task mid-callback and assert that an independent, un-cancelled request still completes promptly. Both fail (time out / "queue stalled") onmainand pass with this change.dart testsuite: 221 passed, 6 network-gated skips, 0 failuresdart formatanddart analyze: cleanCHANGELOG.mdupdated.