HTTP Requests with Dio

Medium PriorityAsked in ~55% of mid-level interviews

4 min read

Networking

http packagedio
Interceptors (auth, retry, logging)❌ Manual✅ First-class
Base URL, default headers, timeoutsPer-requestBaseOptions
Typed errorsThrows generic exceptionsDioException with categorized type
Cancel a request mid-flightCancelToken
Form data / multipartManualBuilt-in
Adapter swap (mock in tests, alternate transport)AwkwardhttpClientAdapter
Plugin ecosystemSmallLarge (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

NeedUse
Add auth header to every requestInterceptor.onRequest
Log every request/responseLogInterceptor (or pretty_dio_logger)
Retry on transient failureCustom interceptor (see Q56) or dio_retry_plus
Cancel in-flight request when user leaves screenCancelToken() + cancelToken.cancel()
Mock in testsReplace _dio.httpClientAdapter or override at DI layer
Stream / download progressdio.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

  1. What is DioException and why does it matter? Dio wraps every failure (timeout, bad status, cancel, send/receive errors) into DioException with a type enum. That lets you write a single error-mapping function (switch (e.type)) instead of stringly-typed error handling. Bonus: it carries the original RequestOptions and Response, so you can retry or inspect.

  2. How do you intercept a request, retry it, and resolve with the new response? Inside onError, call await Dio().fetch(err.requestOptions) to repeat the request with the (possibly modified) options, then handler.resolve(response) to return success to the caller. Be careful not to also call handler.next(err) — exactly one resolution per interceptor.

  3. How do you cancel a request mid-flight? Pass a CancelToken when issuing the request, then call token.cancel(reason) later. In Dio, the cancelled future throws a DioException(type: DioExceptionType.cancel). Pair with widget dispose() for screen-scoped cleanup.

  4. When would you stay on http instead 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.