dio_resilience

A unified Dio interceptor bundling token refresh, retry, timeout, circuit breaker, rate limiting, and fallback — strategies that coordinate so they don't fight each other.

Why it exists

Most HTTP clients handle resilience piecemeal: fresh_dio for token refresh, polly_dart for retry and circuit breaking, manual timeout configuration. When you combine them, they don't know about each other:

  • Retry happens before token refresh completes, wasting an attempt.
  • Circuit breaker trips while the auth token is being refreshed.
  • Timeout fires globally while a single auth refresh is still in flight.

dio_resilience bundles auth, retry, timeout, and (soon) circuit breaker, rate limiting, hedging, and fallback into a single pipeline. Strategies share a context tracking total elapsed time, attempt counts, and in-flight token refreshes. They coordinate so a retry won't fire if auth is already refreshing, and the timeout budget respects both initial and retry attempts.

Quick start

import 'package:dio/dio.dart';
import 'package:dio_resilience/dio_resilience.dart';

final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));

// Build and attach the pipeline
dio.interceptors.add(
  ResiliencePipeline.builder()
    .auth(
      // Use a SEPARATE Dio inside refreshToken — don't recurse through the
      // pipeline you're configuring. A common pattern is a top-level
      // `final _refreshDio = Dio(BaseOptions(baseUrl: 'https://auth.example.com'));`
      refreshToken: (current) async {
        final res = await _refreshDio.post(
          '/token/refresh',
          data: {'refresh_token': current?.refresh},
        );
        return TokenPair(
          access: res.data['access_token'] as String,
          refresh: res.data['refresh_token'] as String,
        );
      },
      preemptiveRefreshWindow: const Duration(minutes: 1),
    )
    .retry(
      maxAttempts: 3,
      backoff: Backoff.exponentialJitter(
        initial: Duration(milliseconds: 200),
        multiplier: 2.0,
        maxDelay: Duration(seconds: 10),
      ),
    )
    .timeout(
      total: Duration(seconds: 30),
      perAttempt: Duration(seconds: 10),
    )
    .build()
    ..attachTo(dio)
);

// Make a request; auth, retry, and timeout are automatic
final res = await dio.get('/users/me');

Per-call overrides

Each strategy can be overridden per-request using extension methods on Options:

Skip retry for a single request

dio.post(
  '/upload',
  options: Options().noRetry(),
);

Skip retry only for specific status codes

dio.post(
  '/charge',
  options: Options().dontRetryOn({409, 422}),
);

Skip retry when a condition matches

dio.post(
  '/withdraw',
  options: Options().dontRetryWhen([
    RetryCondition.dioMessageContains('insufficient_funds'),
  ]),
);

Other shortcuts: .noAuth(), .withTimeout(Duration), .withAccount(AccountId), .retryAttempts(int), .retryOnlyWhen(List<RetryCondition>).

RetryCondition built-in matchers

  • status(int) — Retry only if status code matches exactly.
  • statusIn(Set<int>) — Retry if status code is in the set.
  • statusBetween(int min, int max) — Retry if status code is between min and max (inclusive).
  • networkError() — Retry on network errors (connection, DNS, etc.).
  • timeout() — Retry on any timeout (connect, send, receive).
  • dioMessageContains(String) — Retry if DioException.message contains text (case-insensitive by default).
  • responseBodyContains(String) — Retry if response body stringified contains text.
  • responseField(String path, {Object? equals}) — Retry if dotted JSON path equals a value.
  • predicate(bool Function(RetryEvaluation)) — Custom predicate; full access to error, attempt number, elapsed time.

All conditions are combinable with .or(), .and(), .negated().

Override sealed-class types

  • RetryOverride.disabled — Skip retry.
  • RetryOverride.attempts(int) — Override max attempts.
  • RetryOverride.policy({...}) — Full custom backoff and conditions.
  • AuthOverride.disabled — Skip auth refresh.
  • AuthOverride.account(AccountId) — Use a different account.
  • TimeoutOverride.disabled — Disable timeout enforcement.
  • TimeoutOverride.total(Duration) — Set total budget.
  • TimeoutOverride.perAttempt(Duration) — Set per-attempt budget.

What's in v0.1 vs v0.2

v0.1 (current):

  • Auth strategy: single-flight token refresh, multi-account support, AuthState stream.
  • Retry strategy: all backoff variants (constant, linear, exponential, exponential-jitter), Retry-After header support.
  • Timeout strategy: per-attempt and total budget enforcement.
  • Typed override system: ResilienceOverrides + sealed RetryOverride / AuthOverride / TimeoutOverride.
  • RetryCondition predicates and combinators.
  • Options extensions for per-request overrides.

v0.2 (planned):

  • Circuit breaker strategy.
  • Rate limiter strategy.
  • Hedging strategy (speculative retries).
  • Fallback strategy (fallback responses or endpoints).

Roadmap

  • v0.2: Circuit breaker, rate limiter, hedging, fallback.
  • Testing improvements and real-world feedback.

License

MIT

Libraries

dio_resilience
dio_resilience — a unified Dio interceptor bundling retry, timeout, auth/token-refresh, and (in v0.2) circuit breaker, rate limiter, hedging and fallback strategies that coordinate via a shared pipeline context.