HTTP Requests with Dio
4 min read
Networking
http package | dio | |
|---|---|---|
| Interceptors (auth, retry, logging) | ❌ Manual | ✅ First-class |
| Base URL, default headers, timeouts | Per-request | BaseOptions |
| Typed errors | Throws generic exceptions | DioException with categorized type |
| Cancel a request mid-flight | ❌ | CancelToken |
| Form data / multipart | Manual | Built-in |
| Adapter swap (mock in tests, alternate transport) | Awkward | httpClientAdapter |
| Plugin ecosystem | Small | Large (retry, cookies, pinning, logging) |
Mental model: Dio is what you build on top of http if you needed all of the above — already done, well-tested.
Code in action — typed errors + auth interceptor
class ApiClient {
ApiClient() {
_dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 10),
headers: const {'Content-Type': 'application/json'},
))..interceptors.addAll([
_AuthInterceptor(),
LogInterceptor(requestBody: true, responseBody: true),
_RetryInterceptor(),
]);
}
late final Dio _dio;
Future<User> getUser(String id) async {
try {
final res = await _dio.get('/users/$id');
return User.fromJson(res.data);
} on DioException catch (e) {
throw _toAppException(e);
}
}
AppException _toAppException(DioException e) => switch (e.type) {
DioExceptionType.connectionTimeout => NetworkException('Timeout'),
DioExceptionType.receiveTimeout => NetworkException('Server slow'),
DioExceptionType.badResponse => _fromStatus(e.response),
DioExceptionType.cancel => CancelledException(),
_ => UnknownException(e.message ?? ''),
};
}
// Token refresh on 401
class _AuthInterceptor extends Interceptor {
@override
void onRequest(RequestOptions o, RequestInterceptorHandler h) {
final token = AuthStorage.token;
if (token != null) o.headers['Authorization'] = 'Bearer $token';
h.next(o);
}
@override
Future<void> onError(DioException err, ErrorInterceptorHandler h) async {
if (err.response?.statusCode == 401) {
final fresh = await AuthService.refresh();
if (fresh != null) {
err.requestOptions.headers['Authorization'] = 'Bearer $fresh';
final retry = await Dio().fetch(err.requestOptions);
return h.resolve(retry);
}
}
h.next(err);
}
}
Useful Dio patterns
| Need | Use |
|---|---|
| Add auth header to every request | Interceptor.onRequest |
| Log every request/response | LogInterceptor (or pretty_dio_logger) |
| Retry on transient failure | Custom interceptor (see Q56) or dio_retry_plus |
| Cancel in-flight request when user leaves screen | CancelToken() + cancelToken.cancel() |
| Mock in tests | Replace _dio.httpClientAdapter or override at DI layer |
| Stream / download progress | dio.download(url, path, onReceiveProgress: ...) |
Common mistakes to avoid
// ❌ Creating a new Dio() per request
Future<User> get() => Dio().get(...); // skips all interceptors
// ✅ One Dio instance per client, injected
// ❌ Catching DioException with generic `catch (e)`
try { ... } catch (e) { print(e); } // lose the type info
// ✅ on DioException catch (e) { ... switch (e.type) }
// ❌ Calling handler.next(err) inside an interceptor that already called handler.resolve
// → double-resolve → state corruption
// ✅ Exactly one of next() / resolve() / reject() per interceptor call
// ❌ Forgetting CancelToken on long-running fetches
// User navigates away → request still completes → tries to setState on dead widget
// ✅ Cancel in dispose: _cancel.cancel('disposed')
// ❌ Sharing one CancelToken across unrelated requests
// Cancelling one cancels all of them — usually NOT what you want
// ✅ One CancelToken per logical operation
Interview follow-ups
-
What is
DioExceptionand why does it matter? Dio wraps every failure (timeout, bad status, cancel, send/receive errors) intoDioExceptionwith atypeenum. That lets you write a single error-mapping function (switch (e.type)) instead of stringly-typed error handling. Bonus: it carries the originalRequestOptionsandResponse, so you can retry or inspect. -
How do you intercept a request, retry it, and resolve with the new response? Inside
onError, callawait Dio().fetch(err.requestOptions)to repeat the request with the (possibly modified) options, thenhandler.resolve(response)to return success to the caller. Be careful not to also callhandler.next(err)— exactly one resolution per interceptor. -
How do you cancel a request mid-flight? Pass a
CancelTokenwhen issuing the request, then calltoken.cancel(reason)later. In Dio, the cancelled future throws aDioException(type: DioExceptionType.cancel). Pair with widgetdispose()for screen-scoped cleanup. -
When would you stay on
httpinstead of moving to Dio? When the app makes a handful of one-shot calls (e.g., a hobby app, a simple webhook receiver), or when binary size matters and you can't justify the dependency. For anything with auth, retries, or many endpoints, Dio pays for itself quickly.
How helpful was this content?
Please sign in to rate this article.