dio_auth_interceptor 0.1.0
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.
// 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;
}