With dio in a Flutter gym-membership app, how do you attach the access token with an Interceptor, and what must each handler callback do?
answer
- onRequest, onResponse, onError
- next, resolve or reject
- exactly one handler call
- interceptors run in add order
- async work before handler.next
basics
~10 sAdd an Interceptor whose onRequest sets options.headers['Authorization'] and then calls handler.next(options). Every callback must call exactly one of next, resolve or reject on its handler, or the request never completes.
solid answer
~40 s`dio.interceptors.add(...)` registers an `Interceptor` subclass or an `InterceptorsWrapper` with `onRequest`, `onResponse` and `onError` callbacks, and request interceptors run in the order they were added. In `onRequest` you may await the stored token, set `options.headers['Authorization'] = 'Bearer $token'`, then call `handler.next(options)` to continue. `handler.resolve(response)` short-circuits with a response (a cache hit, a fake), and `handler.reject(exception)` fails the request; the rest of the chain is skipped unless you pass the optional `callFollowing...` flag. The rule is exactly one call per callback: calling twice throws a `StateError` ('The handler has already been called'), and never calling it leaves the request hanging. `onError` receives a `DioException`, where a 401 is the hook for refresh logic.
code
dart · 28 linesimport 'package:dio/dio.dart';
abstract interface class TokenStore {
Future<String?> readAccessToken();
}
class AuthHeaderInterceptor extends Interceptor {
AuthHeaderInterceptor(this._tokens);
final TokenStore _tokens;
@override
Future<void> onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) async {
if (options.extra['public'] != true) {
final token = await _tokens.readAccessToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
}
handler.next(options); // exactly once, on every path
}
}
// dio.interceptors.add(AuthHeaderInterceptor(tokenStore));
// dio.post('/login', data: body, options: Options(extra: {'public': true}));go deeper
Know that an interceptor's onRequest can add a header to every request and must call handler.next(options).
Explain next, resolve and reject for each callback, the one-call rule and its StateError, async callbacks, and insertion order.
Design the interceptor chain (auth, logging, caching) so tokens never leak into logs, public endpoints are flagged, and every path calls the handler.
Decide which cross-cutting concerns belong in interceptors versus repositories so the network layer stays predictable and testable.
## What an interceptor is in dio A dio **interceptor** is an object that sees every request before it is sent, every response before your code gets it, and every error before it is thrown. Each `Dio` has an ordered `interceptors` list; request interceptors run in the order you added them, and the same order is used for responses and errors. You write one in two ways: - subclass `Interceptor` and override `onRequest`, `onResponse` and `onError`; - use `InterceptorsWrapper(onRequest: ..., onResponse: ..., onError: ...)` for a quick inline version. ## The handler contract Each callback receives the data (`RequestOptions`, `Response` or `DioException`) and a **handler**. The handler decides what happens next: | Callback | `next(...)` | `resolve(response)` | `reject(exception)` | |---|---|---|---| | `onRequest` | send the (modified) options on | finish now with this response | fail now with this exception | | `onResponse` | pass the response on | finish with this response | turn it into an error | | `onError` | pass the error on | recover with a response | fail with this (possibly new) error | Rules that follow from the source: 1. **Call exactly one handler method per callback.** A second call throws a `StateError` saying the handler has already been called. 2. **Always call one.** The request's future is completed by the handler; a code path that returns without calling it leaves the caller awaiting forever. 3. `resolve` and `reject` skip the remaining interceptors unless you pass the optional `callFollowing...` flag. 4. Callbacks can be `async`: await the token first, then call the handler. ## Attaching the access token For the gym app, every API call except login needs `Authorization: Bearer <access token>`: - Keep the token in a small `TokenStore` (backed by secure storage) that the interceptor receives through its constructor. - In `onRequest`, read the token, skip public endpoints (a flag in `options.extra` works well), set the header, then call `handler.next(options)`. - Put the auth interceptor **before** a logging interceptor if you want logs to show the header, or after it if logs must never contain tokens. ## Handling a 401 in `onError` `onError` is where a plain token-attaching interceptor detects expiry: `err.response?.statusCode == 401`. The minimal version calls `handler.next(err)` and lets the UI send the user to login. Refreshing the token and replaying the request adds concurrency problems (several requests can fail at once), which is why dio provides a queued variant for that job. ## Built-in interceptors and logging - dio adds an internal interceptor that sets `Content-Type: application/json` when `data` is a `Map`, a `List<Map>` or a `String` and no content type is given. - `LogInterceptor` prints requests and responses. Its `requestHeader` option defaults to `true`, so if it runs **after** your auth interceptor it prints the bearer token. Add it before the auth interceptor, or set `requestHeader: false`, and keep it out of release builds. ## Testing an interceptor Give the `Dio` under test a fake `httpClientAdapter` that records the outgoing `RequestOptions` and returns a canned response. Then assert that the `Authorization` header is present for private calls, absent for calls flagged `public`, and that a throwing token store produces a rejected request instead of a hang. ## Common mistakes - Returning early from `onRequest` for public endpoints **without** calling `handler.next(options)`, so login requests hang. - Calling `handler.next(options)` and then, in a `catch` block, `handler.reject(...)` as well, which throws the "already called" `StateError`. - Creating a new `Dio` for every call, which also recreates the interceptor chain and loses connection reuse. - Reading the token synchronously from a field that is only filled after an async load, so the first requests go out without it.
- How would you serve a cached timetable from an interceptor without hitting the network?In `onRequest`, look up the cache and, on a hit, call `handler.resolve(Response(requestOptions: options, data: cached, statusCode: 200))`. The request never reaches the adapter and the remaining request interceptors are skipped.
- What happens if onRequest awaits a token read that throws?With a plain `Interceptor`, dio ignores the future your callback returns, so the exception becomes an uncaught async error and the handler is never called: the request hangs. Catch it and call `handler.reject(DioException(requestOptions: options, error: e))` so the caller gets a proper failure.
- Does the order in which interceptors are added matter?Yes. Request interceptors run in insertion order, so an interceptor that needs the Authorization header, such as a request signer or a logger that should show it, must be added after the one that sets it.
saying these in an interview costs you the question
- It is fine not to call the handler when nothing needs changing.
- Calling handler.next twice just sends the request twice.
- Interceptor callbacks cannot be async in dio.
- handler.resolve in onRequest still sends the request to the server.
- Each request needs its own Dio so interceptors stay isolated.