With dio in Flutter, how does a CancelToken cancel in-flight requests, and what goes wrong if you reuse a token after cancelling it?
answer
- one token, many requests
- cancel completes whenCancel
- DioExceptionType.cancel
- cancelled tokens stay cancelled
- http has abortTrigger since 1.5
basics
~20 sPass a CancelToken to requests and call cancel() to abort them; each throws a DioException of type cancel. A cancelled token stays cancelled, so any later request using it fails immediately; create a new token per screen or search.
solid answer
~40 sA `CancelToken` is a one-shot switch you pass as `cancelToken:` to any dio call; one token can be shared by several requests. `token.cancel([reason])` completes its `whenCancel` future, dio aborts every request bound to it, and each caller gets a `DioException` whose type is `DioExceptionType.cancel` (check with `CancelToken.isCancel(e)`). Typical uses are cancelling a screen's requests in `dispose` and dropping the previous request when a search query changes. A token cannot be reset: `isCancelled` stays `true`, and dio checks it before sending, so every new request that uses the old token throws the stored cancel error at once. Create a fresh token for each screen instance or each keystroke. Cancelling stops the client waiting; it does not undo work the server already did. `package:http` has had a similar mechanism since 1.5: `AbortableRequest` with an `abortTrigger` future.
code
dart · 27 linesimport 'package:dio/dio.dart';
class ClassSearch {
ClassSearch(this._dio);
final Dio _dio;
CancelToken? _inFlight;
Future<List<dynamic>?> search(String query) async {
_inFlight?.cancel('superseded');
final token = CancelToken(); // never reuse a cancelled token
_inFlight = token;
try {
final response = await _dio.get<List<dynamic>>(
'/classes',
queryParameters: {'q': query},
cancelToken: token,
);
return response.data;
} on DioException catch (e) {
if (CancelToken.isCancel(e)) return null; // a newer query won
rethrow;
}
}
void dispose() => _inFlight?.cancel('disposed');
}go deeper
Know that a CancelToken passed to dio lets you abort requests, for example when leaving a screen.
Explain how cancel surfaces as DioExceptionType.cancel, how CancelToken.isCancel filters it, and why a cancelled token must be replaced.
Scope tokens to screens and queries, keep cancellation away from non-idempotent calls, and reconcile state when a write's outcome is unknown.
Set the team rule for which calls are cancellable and how cancelled writes are reconciled, so behaviour is consistent across features.
## What a `CancelToken` is `CancelToken` is dio's cancellation handle. It wraps a `Completer`: - `cancel([Object? reason])` completes it with a `DioException` built for cancellation; - `whenCancel` is the future dio listens to while a request is in flight; - `isCancelled` and `cancelError` expose the state afterwards; - `CancelToken.isCancel(e)` is a static helper that checks `e.type == DioExceptionType.cancel`. You pass it to any request method, `dio.get(path, cancelToken: token)`, and **one token can control many requests**. ## What happens when you cancel 1. `cancel` completes `whenCancel`. 2. dio races each bound request against that future, so the pending call throws the cancel `DioException`. 3. Your `catch` sees `DioExceptionType.cancel`; in most UIs you ignore it, because the user left or changed the query. Cancelling a token twice is harmless; dio only logs a warning if the second call gives a different reason. ## The reuse trap A token is **one-shot**. Before sending, dio checks `cancelToken.isCancelled` and, if it is `true`, throws the stored cancel error immediately without touching the network. Code that keeps one token in a field, cancels it in `dispose` or on a query change, and then reuses it for the next request will see every later request fail instantly with `cancel`. The fix is structural: - **Per screen**: create the token in `initState` (or the view model's constructor) and cancel it in `dispose`. A new screen instance gets a new token. - **Per search**: on each new query, cancel the previous token and assign a **new** `CancelToken()` before the next request. ## Gym-app examples | Situation | Token strategy | |---|---| | Class-search box that queries as the user types | New token per query; cancel the previous one first | | Booking-history screen loading three endpoints | One token shared by the three requests, cancelled in `dispose` | | Large workout-video download | Its own token behind a Cancel button | | Check-in POST | Usually no cancellation once sent: the server may already have recorded it | The last row matters. Cancelling stops the **client** from waiting; it cannot tell whether the server processed the request. For non-idempotent calls the UI should not assume "cancelled" means "did not happen". ## Cancellation in `package:http` Since `package:http` 1.5, requests that implement `Abortable`, such as `AbortableRequest`, accept an `abortTrigger` future. Completing it (typically through a `Completer`) aborts the request: before sending, `send` fails with `RequestAbortedException`; while streaming, the exception is injected into the response stream. Clients that do not support aborting ignore the trigger, so check the client you use. ## Downloads and progress `dio.download(url, savePath, cancelToken: token, onReceiveProgress: ...)` accepts the same token, which is how a Cancel button on a workout-video download works. Its `deleteOnError` parameter defaults to `true`, so a cancelled download does not leave a partial file behind. ## Cancelling from state holders Whatever owns the request should own the token. A `State` object cancels in `dispose`; a view model or controller cancels in its own dispose hook when the screen that created it goes away. Passing one app-wide token around makes it impossible to cancel one screen's work without cancelling everything. ## Cancellation versus timeouts A timeout is dio deciding the request took too long (`connectionTimeout`, `sendTimeout`, `receiveTimeout`); cancellation is your code deciding the result is no longer wanted. They surface as different `DioExceptionType` values, which lets the UI show "network slow" for one and nothing for the other.
- Why does every request fail immediately with a cancel error after the user returns to a screen?The screen reuses a `CancelToken` that was cancelled in an earlier `dispose`. dio checks `isCancelled` before sending and throws the stored error. Create the token per screen instance or per request batch.
- Does cancelling a check-in POST guarantee the check-in was not recorded?No. Cancellation only stops the client waiting for the response; if the request already reached the server it may have been processed. Treat the outcome as unknown and reconcile, for example by reloading the member's check-ins.
- How do you cancel a package:http request?Use a request type that implements `Abortable`, such as `AbortableRequest`, available since http 1.5, and pass an `abortTrigger` future, usually from a `Completer`. Completing it aborts the request, and the caller sees a `RequestAbortedException`.
saying these in an interview costs you the question
- A cancelled CancelToken resets itself for the next request.
- Each request needs its own token; tokens cannot be shared.
- Cancelling a POST guarantees the server did nothing.
- Cancelled requests surface as a connectionTimeout error.
- package:http offers no way to abort a request.