-
-
Notifications
You must be signed in to change notification settings - Fork 62
Description
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.