Skip to content

New requests sent with stale token while refresh is in-flightΒ #141

@StarProxima

Description

@StarProxima

Description

When a token refresh is in progress (triggered by a 401 response), new requests that enter the interceptor pipeline are still sent with the old (already invalidated) access token. This causes unnecessary 401 responses from the backend.

The single-flight refresh coordination from #130 correctly prevents multiple parallel refreshToken calls, but does not prevent new requests from being dispatched with a stale token during the refresh window.

Problem Details

In fresh_dio, QueuedInterceptor uses separate queues for onRequest, onResponse, and onError (Dio source):

// From Dio's QueuedInterceptor:
final _requestQueue = _TaskQueue<RequestOptions, RequestInterceptorHandler>();
final _responseQueue = _TaskQueue<Response, ResponseInterceptorHandler>();
final _errorQueue = _TaskQueue<DioException, ErrorInterceptorHandler>();

This means onRequest and onError run in parallel. While onError is awaiting a token refresh, new requests pass through onRequest, read the old _token value, and are dispatched to the backend with an already-invalidated access token.

In fresh_http and fresh_graphql, send() / request() are fully concurrent with no queueing, so the same issue applies.

Example Scenario

final dio = Dio();
dio.interceptors.add(fresh);

// Request 1 β†’ sent with old_token β†’ gets 401 β†’ triggers refresh
// While refresh is in-flight, backend has invalidated old_token

// Request 2 arrives β†’ onRequest reads _token (still old_token) β†’ sent β†’ 401
// Request 2's onError detects token already refreshed β†’ retries β†’ 200

// Both succeed, but Request 2 caused an unnecessary 401 round-trip
final results = await Future.wait([
  dio.get('http://example.com/1'),  // 401 β†’ refresh β†’ retry β†’ 200
  dio.get('http://example.com/2'),  // 401 (unnecessary) β†’ retry β†’ 200
]);

Impact

  • Unnecessary 401 responses from the backend for every new request during the refresh window
  • Extra network round-trips β€” each affected request makes 2 calls instead of 1
  • Backend-side noise β€” false "unauthorized" entries in logs, potential rate-limiting or security alerts
  • With refresh token rotation β€” if the single-flight mechanism ever fails to coordinate, parallel refresh calls with an already-rotated refresh token could cause deauthorization

Expected Behavior

New requests arriving while a token refresh is in-flight should wait for the refresh to complete and then be dispatched with the updated token, avoiding unnecessary 401 responses.

Proposed Solution

Add a tokenWaitingRefresh getter to FreshMixin that awaits _refreshFuture (if present) before returning the current token. Use it in onRequest (fresh_dio), send() (fresh_http), and request() (fresh_graphql) instead of the plain token getter:

// In FreshMixin:
@protected
Future<T?> get tokenWaitingRefresh async {
  final pending = _refreshFuture;
  if (pending != null) {
    try {
      await pending;
    } catch (_) {}
  }
  return token;
}

This is a minimal, non-breaking change that piggy-backs on the existing _refreshFuture from #130 without adding new dependencies or altering the refresh lifecycle.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions