Skip to content

Commit

Permalink
Fix: example queued_interceptor_csrftoken.dart (#2134)
Browse files Browse the repository at this point in the history
Provide a more suitable example for queued_interceptor_csrftoken.dart

<!-- Write down your pull request descriptions. -->

### New Pull Request Checklist

- [x] I have read the
[Documentation](https://pub.dev/documentation/dio/latest/)
- [x] I have searched for a similar pull request in the
[project](https://github.com/cfug/dio/pulls) and found none
- [x] I have updated this branch with the latest `main` branch to avoid
conflicts (via merge from master or rebase)
- [ ] I have added the required tests to prove the fix/feature I'm
adding
- [ ] I have updated the documentation (if necessary)
- [x] I have run the tests without failures
- [ ] I have updated the `CHANGELOG.md` in the corresponding package

### Additional context and info (if any)

This example adds 2 different `QueuedInterceptorsWrapper` and the last
one handles `onError` to update csrf token.

---

fixes #2128
  • Loading branch information
seunghwanly authored Mar 19, 2024
1 parent d020224 commit 35533ac
Showing 1 changed file with 153 additions and 83 deletions.
236 changes: 153 additions & 83 deletions example/lib/queued_interceptor_crsftoken.dart
Original file line number Diff line number Diff line change
@@ -1,92 +1,162 @@
import 'dart:async';
import 'dart:convert';
// 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 'package:dio/dio.dart';

void main() async {
final dio = Dio();
// dio instance to request token
final tokenDio = Dio();
String? csrfToken;
dio.options.baseUrl = 'https://seunghwanlytest.mocklab.io/';
tokenDio.options = dio.options;
dio.interceptors.add(
QueuedInterceptorsWrapper(
onRequest: (options, handler) async {
print('send request:path:${options.path},baseURL:${options.baseUrl}');

if (csrfToken == null) {
print('no token,request token firstly...');

final result = await tokenDio.get('/token');

if (result.statusCode != null && result.statusCode! ~/ 100 == 2) {
/// assume `token` is in response body
final body = jsonDecode(result.data) as Map<String, dynamic>?;

if (body != null && body.containsKey('data')) {
options.headers['csrfToken'] = csrfToken = body['data']['token'];
print('request token succeed, value: $csrfToken');
print(
'continue to perform request:path:${options.path},baseURL:${options.path}',
);
return handler.next(options);
/// HTML example:
/// ``` html
/// <input type="hidden" name="XSRF_TOKEN" value=${cachedCSRFToken} />
/// ```
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.reject(
DioException(requestOptions: result.requestOptions),
true,
);
}

options.headers['csrfToken'] = csrfToken;
return handler.next(options);
},
onError: (error, handler) async {
/// Assume 401 stands for token expired
if (error.response?.statusCode == 401) {
print('the token has expired, need to receive new token');
final options = error.response!.requestOptions;

/// assume receiving the token has no errors
/// to check `null-safety` and error handling
/// please check inside the [onRequest] closure
final tokenResult = await tokenDio.get('/token');

/// update [csrfToken]
/// assume `token` is in response body
final body = jsonDecode(tokenResult.data) as Map<String, dynamic>?;
options.headers['csrfToken'] = csrfToken = body!['data']['token'];

if (options.headers['csrfToken'] != null) {
print('the token has been updated');

/// since the api has no state, force to pass the 401 error
/// by adding query parameter
final originResult = await dio.fetch(options..path += '&pass=true');
if (originResult.statusCode != null &&
originResult.statusCode! ~/ 100 == 2) {
return handler.resolve(originResult);
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;
}
}
print('the token has not been updated');
return handler.reject(
DioException(requestOptions: options),
);
}
return handler.next(error);
},
),
);
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);
}

FutureOr<void> onResult(d) {
print('request ok!');
}
cachedCSRFToken = updatedToken;

/// assume `/test?tag=2` path occurs the authorization error (401)
/// and token to be updated
await dio.get('/test?tag=1').then(onResult);
await dio.get('/test?tag=2').then(onResult);
await dio.get('/test?tag=3').then(onResult);
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',

/// Pretend the Server has generated CSRF token
/// and passed it to the client.
queryParameters: {
headerKey: 'fbf07f2b-b957-4555-88a2-3d3e30e5fa64',
},
);
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/',
);
printLog(2, createResult.realUri.path);
}

0 comments on commit 35533ac

Please sign in to comment.