Skip to content

Conversation

@AristideVB
Copy link

Summary

Fixes #126 - Concurrent requests cause multiple token refresh events

This PR implements a zero-dependency single-flight token refresh mechanism that ensures only one refresh operation occurs when multiple requests fail simultaneously due to an expired/invalid access token. The fix is implemented in both fresh_dio and fresh_graphql packages, with shared coordination logic in the core fresh package.

Problem

When multiple HTTP requests receive 401 responses (or GraphQL UNAUTHENTICATED errors) simultaneously, each request independently triggers refreshToken, resulting in multiple concurrent refresh operations instead of a single one:

final results = await Future.wait([
  dio.get('http://example.com/1'),  // 401 → triggers refresh
  dio.get('http://example.com/2'),  // 401 → triggers refresh (again!)
  dio.get('http://example.com/3'),  // 401 → triggers refresh (again!)
]);
// Result: refreshToken is called 3 times instead of 1

Solution

This PR implements a single-flight refresh pattern directly in the FreshMixin:

Key Design Decisions

Aspect PR #127 This PR
Dependencies Adds synchronized package ✅ Zero new dependencies
Scope fresh_dio only ✅ Both fresh_dio AND fresh_graphql
Coordination Lock-based with identical() check ✅ Future-based with token comparison
Code Location In Fresh interceptor ✅ In core FreshMixin (shared)
Test Coverage Basic test ✅ 5 comprehensive test scenarios per package

How It Works

  1. singleFlightRefresh method in FreshMixin: A new method that coordinates concurrent refresh attempts
  2. Token capture at request time: Each request stores the token it was made with
  3. Smart detection: When a 401/UNAUTHENTICATED is received, the coordinator checks:
    • Has the token already changed since this request was made? → Use new token, skip refresh
    • Is a refresh already in-flight? → Await the same future
    • Otherwise → Start a new refresh
  4. Proper cleanup: The finally block ensures _refreshFuture is cleared even on exceptions

Why This Approach is Better

  • No external dependencies: Uses only Dart's built-in Future for coordination
  • Works with QueuedInterceptor: Dio's interceptor serializes requests, so we compare tokens instead of just checking for in-flight refreshes
  • Covers fresh_graphql too: GraphQL streams run concurrently, and this solution handles both execution models
  • Comprehensive error handling: Properly handles RevokeTokenException, generic exceptions, and ensures no hangs

Changes

Core fresh package

  • Added singleFlightRefresh(refreshAction, {tokenBeforeRefresh}) method to FreshMixin
  • Added _refreshFuture field to track in-flight refresh operations
  • Added _performRefresh helper with proper try/catch/finally cleanup

fresh_dio package

  • Modified onRequest to store the token used for each request in options.extra['_fresh_request_token']
  • Modified _tryRefresh to use singleFlightRefresh with tokenBeforeRefresh parameter

fresh_graphql package

  • Modified request() to capture tokenUsedForRequest at stream start
  • Uses singleFlightRefresh with tokenBeforeRefresh for coordination
  • Added catch-all exception handler to yield original response (no hangs)

Test Coverage

Added 5 comprehensive test scenarios for each package:

Test Scenario Description
Parallel 401s 3 concurrent requests all get 401 → refreshToken called exactly once
In-flight waiting Requests arriving while refresh is in-flight await the same refresh
RevokeTokenException Token revoked once, all requests complete without hanging
Generic exception State resets properly, subsequent requests can trigger new refresh
Subsequent refresh After successful refresh, later 401s correctly trigger new refreshes

Test Results

  • fresh: 15 tests ✅
  • fresh_dio: 32 tests ✅ (5 new)
  • fresh_graphql: 17 tests ✅ (5 new)

Breaking Changes

None. The fix is backward compatible.

Checklist

  • Fixes Concurrent requests cause multiple token refresh events #126
  • No new dependencies added
  • Covers both fresh_dio and fresh_graphql
  • Comprehensive test coverage
  • All existing tests pass
  • No lint warnings (except expected path dependency warning for local testing)
  • CHANGELOG entries added for all three packages

Note

The pubspec.yaml files currently use path dependencies for local development. Before merging, these should be updated to proper version constraints.

Fixes felangel#126

- Add singleFlightRefresh method to FreshMixin in core fresh package
- Implement coordination in fresh_dio using token comparison
- Implement coordination in fresh_graphql for parallel streams
- Ensure only one refresh occurs when multiple 401s happen concurrently
- Add comprehensive tests for both packages (5 scenarios each)

Zero new dependencies. Covers both fresh_dio and fresh_graphql.
@AristideVB AristideVB requested a review from felangel as a code owner January 27, 2026 22:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Concurrent requests cause multiple token refresh events

1 participant