dio_auth_interceptor 0.1.0 copy "dio_auth_interceptor: ^0.1.0" to clipboard
dio_auth_interceptor: ^0.1.0 copied to clipboard

Drop-in Dio & http interceptor for JWT refresh with a tested single-flight queue: concurrent 401s trigger exactly one refresh call, zero boilerplate.

example/main.dart

// An end-to-end demo against a real local HTTP server (not a mocked
// adapter): a stale access token gets a real 401 over a real socket, and
// DioAuthInterceptor refreshes it exactly once even though 10 requests hit
// it at the same time.
//
// Run with: dart run example/main.dart
import 'dart:async';
import 'dart:convert';
import 'dart:io';

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

const _staleAccessToken = 'expired-access-token';
const _freshAccessToken = 'fresh-access-token';
const _refreshToken = 'my-refresh-token';

Future<void> main() async {
  final server = await _startMockServer();
  final baseUrl = 'http://${server.address.host}:${server.port}';
  print('Mock server listening on $baseUrl');

  var refreshCallCount = 0;

  // A plain Dio with no DioAuthInterceptor of its own — used only to call
  // /auth/refresh, so the refresh call can never recurse into itself.
  final plainDio = Dio(BaseOptions(baseUrl: baseUrl));

  final dio = Dio(BaseOptions(baseUrl: baseUrl))
    ..interceptors.add(
      DioAuthInterceptor(
        refreshToken: () async {
          refreshCallCount++;
          final response = await plainDio.post<Map<String, dynamic>>(
            '/auth/refresh',
            data: {'refreshToken': _refreshToken},
          );
          return response.data!['accessToken'] as String;
        },
        onRefreshFailed: (error, stackTrace) => print('Refresh failed: $error'),
      ),
    );

  print('Firing 10 concurrent requests with a stale access token...');
  final responses = await Future.wait(
    List.generate(
      10,
      (_) => dio.get<Map<String, dynamic>>(
        '/data',
        options:
            Options(headers: {'Authorization': 'Bearer $_staleAccessToken'}),
      ),
    ),
  );

  final okCount = responses.where((r) => r.statusCode == 200).length;
  print('$okCount/10 requests succeeded after a single transparent refresh.');
  print('Total refreshToken() calls: $refreshCallCount (expected: 1)');

  await server.close(force: true);
}

/// A tiny real HTTP server (dart:io, no extra dependency) simulating a
/// backend that rejects the stale token and issues a new one on refresh.
Future<HttpServer> _startMockServer() async {
  final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);

  unawaited(
    server.forEach((request) async {
      request.response.headers.contentType = ContentType.json;

      if (request.uri.path == '/auth/refresh' && request.method == 'POST') {
        await request.drain<void>();
        request.response.write(jsonEncode({'accessToken': _freshAccessToken}));
        await request.response.close();
        return;
      }

      if (request.uri.path == '/data') {
        final auth = request.headers.value('authorization');
        if (auth == 'Bearer $_freshAccessToken') {
          request.response.write(jsonEncode({'ok': true}));
        } else {
          request.response.statusCode = HttpStatus.unauthorized;
          request.response.write(jsonEncode({'error': 'unauthorized'}));
        }
        await request.response.close();
        return;
      }

      request.response.statusCode = HttpStatus.notFound;
      await request.response.close();
    }),
  );

  return server;
}
0
likes
160
points
79
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Drop-in Dio & http interceptor for JWT refresh with a tested single-flight queue: concurrent 401s trigger exactly one refresh call, zero boilerplate.

Repository (GitHub)
View/report issues

Topics

#dio #http #interceptor #authentication #jwt

License

MIT (license)

Dependencies

dio, http

More

Packages that depend on dio_auth_interceptor