diff --git a/dio/README-ZH.md b/dio/README-ZH.md
index a48ec6cbb..5b6e3337e 100644
--- a/dio/README-ZH.md
+++ b/dio/README-ZH.md
@@ -520,7 +520,7 @@ print(response.data); // 'fake data'
`csrfToken` 都为 null,所以它们都需要去请求 `csrfToken`,这会导致 `csrfToken` 被请求多次。
为了避免不必要的重复请求,可以使用 `QueuedInterceptor`, 这样只需要第一个请求处理一次即可。
-完整的示例代码请点击 [这里](../example/lib/queued_interceptor_crsftoken.dart).
+完整的示例代码请点击 [这里](../example_dart/lib/queued_interceptor_crsftoken.dart).
#### 日志拦截器
diff --git a/dio/README.md b/dio/README.md
index 8cb01fdf2..b14bdd118 100644
--- a/dio/README.md
+++ b/dio/README.md
@@ -528,7 +528,7 @@ we need to request a csrfToken first, and then perform the network request,
because the request csrfToken progress is asynchronous,
so we need to execute this async request in request interceptor.
-For the complete code see [here](../example/lib/queued_interceptor_crsftoken.dart).
+For the complete code see [here](../example_dart/lib/queued_interceptor_crsftoken.dart).
#### LogInterceptor
diff --git a/example_dart/lib/queued_interceptor_crsftoken.dart b/example_dart/lib/queued_interceptor_crsftoken.dart
index 1e60ba86c..e1cfd8175 100644
--- a/example_dart/lib/queued_interceptor_crsftoken.dart
+++ b/example_dart/lib/queued_interceptor_crsftoken.dart
@@ -1,164 +1,172 @@
-// ignore: dangling_library_doc_comments
-/// CSRF Token Example
-///
-/// Add interceptors to handle CSRF token.
-/// - token update
-/// - retry policy
-///
-/// Scenario:
-/// 1. Client access to the Server by using `GET` method.
-/// 2. Server generates CSRF token and sends it to the client.
-/// 3. Client make a request to the Server by using `POST` method with the CSRF token.
-/// 4. If the CSRF token is invalid, the Server returns 401 status code.
-/// 5. Client requests a new CSRF token and retries the request.
-import 'dart:developer';
+import 'dart:convert';
+import 'dart:math';
import 'package:dio/dio.dart';
void main() async {
- /// HTML example:
- /// ``` html
- ///
- /// ```
- const String cookieKey = 'XSRF_TOKEN';
-
- /// Header key for CSRF token
- const String headerKey = 'X-Csrf-Token';
-
- String? cachedCSRFToken;
-
- void printLog(
- int index,
- String path,
- ) =>
- log(
- '''
-#$index
-- Path: '$path'
-- CSRF Token: $cachedCSRFToken
-''',
- name: 'queued_interceptor_csrftoken.dart',
- );
-
- final dio = Dio()
- ..options.baseUrl = 'https://httpbun.com/'
- ..interceptors.addAll(
- [
- /// Handles CSRF token
- QueuedInterceptorsWrapper(
- /// Adds CSRF token to headers, if it exists
- onRequest: (requestOptions, handler) {
- if (cachedCSRFToken != null) {
- requestOptions.headers[headerKey] = cachedCSRFToken;
- requestOptions.headers['Set-Cookie'] =
- '$cookieKey=$cachedCSRFToken';
- }
- return handler.next(requestOptions);
- },
-
- /// Update CSRF token from [response] headers, if it exists
- onResponse: (response, handler) {
- final token = response.headers.value(headerKey);
-
- if (token != null) {
- cachedCSRFToken = token;
- }
- return handler.resolve(response);
- },
-
- onError: (error, handler) async {
- if (error.response == null) {
- return handler.next(error);
- }
-
- /// When request fails with 401 status code, request new CSRF token
- if (error.response?.statusCode == 401) {
- try {
- final tokenDio = Dio(
- BaseOptions(baseUrl: error.requestOptions.baseUrl),
- );
-
- /// Generate CSRF token
- ///
- /// This is a MOCK REQUEST to generate a CSRF token.
- /// In a real-world scenario, this should be generated by the server.
- final result = await tokenDio.post(
- '/response-headers',
- queryParameters: {
- headerKey: '94d6d1ca-fa06-468f-a25c-2f769d04c26c',
- },
- );
-
- if (result.statusCode == null ||
- result.statusCode! ~/ 100 != 2) {
- throw DioException(requestOptions: result.requestOptions);
- }
-
- final updatedToken = result.headers.value(headerKey);
- if (updatedToken == null) {
- throw ArgumentError.notNull(headerKey);
- }
-
- cachedCSRFToken = updatedToken;
-
- return handler.next(error);
- } on DioException catch (e) {
- return handler.reject(e);
- }
- }
- },
- ),
-
- /// Retry the request when 401 occurred
- QueuedInterceptorsWrapper(
- onError: (error, handler) async {
- if (error.response != null && error.response!.statusCode == 401) {
- final retryDio = Dio(
- BaseOptions(baseUrl: error.requestOptions.baseUrl),
- );
-
- if (error.requestOptions.headers.containsKey(headerKey) &&
- error.requestOptions.headers[headerKey] != cachedCSRFToken) {
- error.requestOptions.headers[headerKey] = cachedCSRFToken;
- }
-
- /// In real-world scenario,
- /// the request should be requested with [error.requestOptions]
- /// using [fetch] method.
- /// ``` dart
- /// final result = await retryDio.fetch(error.requestOptions);
- /// ```
- final result = await retryDio.get('/mix/s=200');
-
- return handler.resolve(result);
- }
- },
- ),
- ],
- );
-
- /// Make Requests
- printLog(0, 'initial');
-
- /// #1 Access to the Server
- final accessResult = await dio.get(
- '/response-headers',
+ final tokenManager = TokenManager();
- /// Pretend the Server has generated CSRF token
- /// and passed it to the client.
- queryParameters: {
- headerKey: 'fbf07f2b-b957-4555-88a2-3d3e30e5fa64',
- },
+ final dio = Dio(
+ BaseOptions(
+ baseUrl: 'https://httpbun.com',
+ ),
);
- printLog(1, accessResult.realUri.path);
-
- /// #2 Make a request(POST) to the Server
- ///
- /// Pretend the token has expired.
- ///
- /// Then the interceptor will request a new CSRF token
- final createResult = await dio.post(
- '/mix/s=401/',
+
+ dio.interceptors.add(
+ QueuedInterceptorsWrapper(
+ onRequest: (requestOptions, handler) {
+ print(
+ '''
+[onRequest] ${requestOptions.hashCode} / time: ${DateTime.now().toIso8601String()}
+\tPath: ${requestOptions.path}
+\tHeaders: ${requestOptions.headers}
+ ''',
+ );
+
+ // In case, you have 'refresh_token' and needs to refresh your 'access_token',
+ // request a new 'access_token' and update from here.
+
+ if (tokenManager.accessToken != null) {
+ requestOptions.headers['Authorization'] =
+ 'Bearer ${tokenManager.accessToken}';
+ }
+
+ return handler.next(requestOptions);
+ },
+ onResponse: (response, handler) {
+ print('''
+[onResponse] ${response.requestOptions.hashCode} / time: ${DateTime.now().toIso8601String()}
+\tStatus: ${response.statusCode}
+\tData: ${response.data}
+ ''');
+
+ return handler.resolve(response);
+ },
+ onError: (error, handler) async {
+ final statusCode = error.response?.statusCode;
+ print(
+ '''
+[onError] ${error.requestOptions.hashCode} / time: ${DateTime.now().toIso8601String()}
+\tStatus: $statusCode
+ ''',
+ );
+
+ // This example only handles the '401' status code,
+ // The more complex scenario should handle more status codes e.g. '403', '404', etc.
+ if (statusCode != 401) {
+ return handler.resolve(error.response!);
+ }
+
+ // To prevent repeated requests to the 'Authentication Server'
+ // to update our 'access_token' with parallel requests,
+ // we need to compare with the previously requested 'access_token'.
+ final requestedAccessToken =
+ error.requestOptions.headers['Authorization'];
+ if (requestedAccessToken == tokenManager.accessToken) {
+ final tokenRefreshDio = Dio()
+ ..options.baseUrl = 'https://httpbun.com';
+
+ final response = await tokenRefreshDio.post(
+ '/mix/s=201/b64=${base64.encode(
+ jsonEncode(AuthenticationServer.generate()).codeUnits,
+ )}',
+ );
+ tokenRefreshDio.close();
+
+ // Treat codes other than 2XX as rejected.
+ if (response.statusCode == null || response.statusCode! ~/ 100 != 2) {
+ return handler.reject(error);
+ }
+
+ final body = jsonDecode(response.data) as Map;
+ if (!body.containsKey('access_token')) {
+ return handler.reject(error);
+ }
+
+ final token = body['access_token'] as String;
+ tokenManager.setAccessToken(token, error.requestOptions.hashCode);
+ }
+
+ /// The authorization has been resolved so and try again with the request.
+ final retried = await dio.fetch(
+ error.requestOptions
+ ..path = '/mix/s=200'
+ ..headers = {
+ 'Authorization': 'Bearer ${tokenManager.accessToken}',
+ },
+ );
+
+ // Treat codes other than 2XX as rejected.
+ if (retried.statusCode == null || retried.statusCode! ~/ 100 != 2) {
+ return handler.reject(error);
+ }
+
+ return handler.resolve(error.response!);
+ },
+ ),
);
- printLog(2, createResult.realUri.path);
+
+ await Future.wait([
+ dio.post('/mix/s=401'),
+ dio.post('/mix/s=401'),
+ dio.post('/mix/s=200'),
+ ]);
+
+ tokenManager.printHistory();
+
+ dio.close();
+}
+
+typedef TokenHistory = ({
+ String? previous,
+ String? current,
+ DateTime updatedAt,
+ int updatedBy,
+});
+
+/// Pretend as 'Authentication Server' that generates access token and refresh token
+class AuthenticationServer {
+ static Map generate() => {
+ 'access_token': _generateUuid(),
+ 'refresh_token': _generateUuid(),
+ };
+
+ static String _generateUuid() {
+ final random = Random.secure();
+ final bytes = List.generate(8, (_) => random.nextInt(256));
+ return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
+ }
+}
+
+class TokenManager {
+ static String? _accessToken;
+
+ static final List _history = [];
+
+ String? get accessToken => _accessToken;
+
+ void printHistory() {
+ print('=== Token History ===');
+ for (int i = 0; i < _history.length; i++) {
+ final entry = _history[i];
+ print('''
+[$i]\tupdated token: ${entry.previous} → ${entry.current}
+\tupdated at: ${entry.updatedAt.toIso8601String()}
+\tupdated by: ${entry.updatedBy}
+ ''');
+ }
+ }
+
+ void setAccessToken(String? token, int instanceId) {
+ final previous = _accessToken;
+ _accessToken = token;
+ _history.add(
+ (
+ previous: previous,
+ current: _accessToken,
+ updatedAt: DateTime.now(),
+ updatedBy: instanceId,
+ ),
+ );
+ }
}