skip to content

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?

level: middleimportance: must knowfreq 55%

answer

  1. onRequest, onResponse, onError
  2. next, resolve or reject
  3. exactly one handler call
  4. interceptors run in add order
  5. async work before handler.next

basics

~10 s

Add 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 lines
dart
import '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

for a junior

Know that an interceptor's onRequest can add a header to every request and must call handler.next(options).

for a middle

Explain next, resolve and reject for each callback, the one-call rule and its StateError, async callbacks, and insertion order.

for a senior

Design the interceptor chain (auth, logging, caching) so tokens never leak into logs, public endpoints are flagged, and every path calls the handler.

for a principal

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.