From a8871add6f7804f1fff2523f939f0e76363047b9 Mon Sep 17 00:00:00 2001 From: seunghwanly Date: Tue, 24 Dec 2024 11:58:50 +0900 Subject: [PATCH 1/7] fix: example under misconception --- .../lib/queued_interceptor_crsftoken.dart | 319 +++++++++--------- 1 file changed, 163 insertions(+), 156 deletions(-) diff --git a/example_dart/lib/queued_interceptor_crsftoken.dart b/example_dart/lib/queued_interceptor_crsftoken.dart index 1e60ba86c..1a73a609a 100644 --- a/example_dart/lib/queued_interceptor_crsftoken.dart +++ b/example_dart/lib/queued_interceptor_crsftoken.dart @@ -1,164 +1,171 @@ -// 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' + /// Make a request for 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 '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!); + } + + /// Consider [dio] can be requested in parallel. + /// + /// To prevent repeated requests to the 'Authentication Server' + /// to update our 'access_token', + /// we can 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( + 'https://httpbun.com/mix/s=201/b64=${base64.encode( + jsonEncode(AuthenticationServer.generate()).codeUnits, + )}', + ); + 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); + tokenRefreshDio.close(); + } + + /// Pretend authorization has been resolved and try again + final retried = await dio.fetch( + error.requestOptions + ..path = '/mix/s=200' + ..headers = { + 'Authorization': 'Bearer ${tokenManager.accessToken}', + }, + ); + + 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, + ), + ); + } } From 9fc8bc6b3eed3cf7e15a500bb1e9367ff7e70fd1 Mon Sep 17 00:00:00 2001 From: seunghwanly Date: Tue, 24 Dec 2024 12:01:22 +0900 Subject: [PATCH 2/7] chore: file path /example directory has been updated to /example_dart --- dio/README-ZH.md | 36 ++++++++++++++++++++---------------- dio/README.md | 42 +++++++++++++++++++++++------------------- 2 files changed, 43 insertions(+), 35 deletions(-) diff --git a/dio/README-ZH.md b/dio/README-ZH.md index a48ec6cbb..6e6ba608c 100644 --- a/dio/README-ZH.md +++ b/dio/README-ZH.md @@ -10,7 +10,7 @@ dio 是一个强大的 HTTP 网络请求库,支持全局配置、Restful API > 别忘了为你发布的与 dio 相关的 package 添加 > [#dio](https://pub.flutter-io.cn/packages?q=topic%3Adio) 分类标签! -> 了解更多:https://dart.cn/tools/pub/pubspec#topics +> 了解更多:
内容列表 @@ -110,7 +110,7 @@ void getHttp() async { ## 示例 -### 发起一个 `GET` 请求 : +### 发起一个 `GET` 请求 ```dart import 'package:dio/dio.dart'; @@ -130,19 +130,19 @@ void request() async { } ``` -### 发起一个 `POST` 请求: +### 发起一个 `POST` 请求 ```dart response = await dio.post('/test', data: {'id': 12, 'name': 'dio'}); ``` -### 发起多个并发请求: +### 发起多个并发请求 ```dart response = await Future.wait([dio.post('/info'), dio.get('/token')]); ``` -### 下载文件: +### 下载文件 ```dart response = await dio.download( @@ -151,7 +151,7 @@ response = await dio.download( ); ``` -### 以流的方式接收响应数据: +### 以流的方式接收响应数据 ```dart final rs = await dio.get( @@ -161,7 +161,7 @@ final rs = await dio.get( print(rs.data.stream); // 响应流 ``` -### 以二进制数组的方式接收响应数据: +### 以二进制数组的方式接收响应数据 ```dart final rs = await dio.get( @@ -171,7 +171,7 @@ final rs = await dio.get( print(rs.data); // 类型: List ``` -### 发送 `FormData`: +### 发送 `FormData` ```dart final formData = FormData.fromMap({ @@ -181,7 +181,7 @@ final formData = FormData.fromMap({ final response = await dio.post('/info', data: formData); ``` -### 通过 `FormData` 上传多个文件: +### 通过 `FormData` 上传多个文件 ```dart final formData = FormData.fromMap({ @@ -196,7 +196,7 @@ final formData = FormData.fromMap({ final response = await dio.post('/info', data: formData); ``` -### 监听发送(上传)数据进度: +### 监听发送(上传)数据进度 ```dart final response = await dio.post( @@ -208,7 +208,7 @@ final response = await dio.post( ); ``` -### 以流的形式提交二进制数据: +### 以流的形式提交二进制数据 ```dart // Binary data @@ -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). #### 日志拦截器 @@ -630,7 +630,7 @@ dio.post( ## 发送 FormData -Dio 支持发送 `FormData`, 请求数据将会以 `multipart/form-data` 方式编码, +Dio 支持发送 `FormData`, 请求数据将会以 `multipart/form-data` 方式编码, `FormData` 中可以包含一个或多个文件。 ```dart @@ -657,7 +657,7 @@ final formDataWithBoundaryName = FormData( ### 多文件上传 -多文件上传时,通过给 key 加中括号 `[]` 方式作为文件数组的标记,大多数后台也会通过 `key[]` 来读取多个文件。 +多文件上传时,通过给 key 加中括号 `[]` 方式作为文件数组的标记,大多数后台也会通过 `key[]` 来读取多个文件。 然而 RFC 标准中并没有规定多文件上传必须要使用 `[]`,关键在于后台与客户端之间保持一致。 ```dart @@ -692,6 +692,7 @@ formData.files.addAll([ 常见的错误做法是将 `FormData` 赋值给一个共享变量,在每次请求中都使用这个变量。 这样的操作会加大 **无法序列化** 的错误出现的可能性。 你可以像以下的代码一样编写你的请求以避免出现这样的错误: + ```dart Future _repeatedlyRequest() async { Future createFormData() async { @@ -759,13 +760,16 @@ dio.httpClientAdapter = HttpClientAdapter(); ``` 如果你需要单独使用对应平台的适配器: -- 对于 Web 平台 +* 对于 Web 平台 + ```dart import 'package:dio/browser.dart'; // ... dio.httpClientAdapter = BrowserHttpClientAdapter(); ``` + - 对于原生平台: + ```dart import 'package:dio/io.dart'; // ... @@ -915,7 +919,7 @@ token.cancel('cancelled'); ## 继承 Dio class `Dio` 是一个拥有工厂构造函数的接口类,因此不能直接继承 `Dio`, -但是可以继承 `DioForNative` 或 `DioForBrowser`: +但是可以继承 `DioForNative` 或 `DioForBrowser`: ```dart import 'package:dio/dio.dart'; diff --git a/dio/README.md b/dio/README.md index 8cb01fdf2..971e70428 100644 --- a/dio/README.md +++ b/dio/README.md @@ -8,11 +8,11 @@ Language: English | [简体中文](README-ZH.md) A powerful HTTP networking package for Dart/Flutter, supports Global configuration, Interceptors, FormData, Request cancellation, File uploading/downloading, -Timeout, Custom adapters, Transformers, etc. +Timeout, Custom adapters, Transformers, etc. > Don't forget to add [#dio](https://pub.dev/packages?q=topic%3Adio) > topic to your published dio related packages! -> See more: https://dart.dev/tools/pub/pubspec#topics +> See more:
Table of content @@ -94,7 +94,7 @@ in [here](https://github.com/cfug/dio/issues/347). ## Examples -### Performing a `GET` request: +### Performing a `GET` request ```dart import 'package:dio/dio.dart'; @@ -114,19 +114,19 @@ void request() async { } ``` -### Performing a `POST` request: +### Performing a `POST` request ```dart response = await dio.post('/test', data: {'id': 12, 'name': 'dio'}); ``` -### Performing multiple concurrent requests: +### Performing multiple concurrent requests ```dart response = await Future.wait([dio.post('/info'), dio.get('/token')]); ``` -### Downloading a file: +### Downloading a file ```dart response = await dio.download( @@ -135,7 +135,7 @@ response = await dio.download( ); ``` -### Get response stream: +### Get response stream ```dart final rs = await dio.get( @@ -145,7 +145,7 @@ final rs = await dio.get( print(rs.data.stream); // Response stream. ``` -### Get response with bytes: +### Get response with bytes ```dart final rs = await Dio().get>( @@ -155,7 +155,7 @@ final rs = await Dio().get>( print(rs.data); // Type: List. ``` -### Sending a `FormData`: +### Sending a `FormData` ```dart final formData = FormData.fromMap({ @@ -165,7 +165,7 @@ final formData = FormData.fromMap({ final response = await dio.post('/info', data: formData); ``` -### Uploading multiple files to server by FormData: +### Uploading multiple files to server by FormData ```dart final formData = FormData.fromMap({ @@ -180,7 +180,7 @@ final formData = FormData.fromMap({ final response = await dio.post('/info', data: formData); ``` -### Listening the uploading progress: +### Listening the uploading progress ```dart final response = await dio.post( @@ -192,7 +192,7 @@ final response = await dio.post( ); ``` -### Post binary data with Stream: +### Post binary data with Stream ```dart // Binary data @@ -214,7 +214,7 @@ See all examples code [here](example). ## Dio APIs -### Creating an instance and set default configs. +### Creating an instance and set default configs > It is recommended to use a singleton of `Dio` in projects, which can manage configurations like headers, base urls, > and timeouts consistently. @@ -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 @@ -707,6 +707,7 @@ You should make a new `FormData` or `MultipartFile` every time in repeated reque A typical wrong behavior is setting the `FormData` as a variable and using it in every request. It can be easy for the *Cannot finalize* exceptions to occur. To avoid that, write your requests like the below code: + ```dart Future _repeatedlyRequest() async { Future createFormData() async { @@ -725,7 +726,7 @@ Future _repeatedlyRequest() async { `Transformer` allows changes to the request/response data before it is sent/received to/from the server. -Dio has already implemented a `BackgroundTransformer` as default, +Dio has already implemented a `BackgroundTransformer` as default, which calls `jsonDecode` in an isolate if the response is larger than 50 KB. If you want to customize the transformation of request/response data, you can provide a `Transformer` by your self, @@ -757,20 +758,23 @@ dio.httpClientAdapter = HttpClientAdapter(); ``` If you want to use platform adapters explicitly: -- For the Web platform: +* For the Web platform: + ```dart import 'package:dio/browser.dart'; // ... dio.httpClientAdapter = BrowserHttpClientAdapter(); ``` + - For native platforms: + ```dart import 'package:dio/io.dart'; // ... dio.httpClientAdapter = IOHttpClientAdapter(); ``` -[Here](../example/lib/adapter.dart) is a simple example to custom adapter. +[Here](../example/lib/adapter.dart) is a simple example to custom adapter. ### Using proxy @@ -810,7 +814,7 @@ the certificates protecting the TLS connection to the server are the ones you ex The intention is to reduce the chance of a man-in-the-middle attack. The theory is covered by [OWASP](https://owasp.org/www-community/controls/Certificate_and_Public_Key_Pinning). -_Server Response Certificate_ +*Server Response Certificate* Unlike other methods, this one works with the certificate of the server itself. @@ -849,7 +853,7 @@ openssl s_client -servername pinning-test.badssl.com -connect pinning-test.badss # (remove the formatting, keep only lower case hex characters to match the `sha256` above) ``` -_Certificate Authority Verification_ +*Certificate Authority Verification* These methods work well when your server has a self-signed certificate, but they don't work for certificates issued by a 3rd party like AWS or Let's Encrypt. From 0c668a35a9bd11c6a5f031921d1b12148f9cbdd5 Mon Sep 17 00:00:00 2001 From: Seunghwan Lee Date: Tue, 24 Dec 2024 12:12:18 +0900 Subject: [PATCH 3/7] chore: IDE automation fixes --- dio/README-ZH.md | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/dio/README-ZH.md b/dio/README-ZH.md index 6e6ba608c..5b6e3337e 100644 --- a/dio/README-ZH.md +++ b/dio/README-ZH.md @@ -10,7 +10,7 @@ dio 是一个强大的 HTTP 网络请求库,支持全局配置、Restful API > 别忘了为你发布的与 dio 相关的 package 添加 > [#dio](https://pub.flutter-io.cn/packages?q=topic%3Adio) 分类标签! -> 了解更多: +> 了解更多:https://dart.cn/tools/pub/pubspec#topics
内容列表 @@ -110,7 +110,7 @@ void getHttp() async { ## 示例 -### 发起一个 `GET` 请求 +### 发起一个 `GET` 请求 : ```dart import 'package:dio/dio.dart'; @@ -130,19 +130,19 @@ void request() async { } ``` -### 发起一个 `POST` 请求 +### 发起一个 `POST` 请求: ```dart response = await dio.post('/test', data: {'id': 12, 'name': 'dio'}); ``` -### 发起多个并发请求 +### 发起多个并发请求: ```dart response = await Future.wait([dio.post('/info'), dio.get('/token')]); ``` -### 下载文件 +### 下载文件: ```dart response = await dio.download( @@ -151,7 +151,7 @@ response = await dio.download( ); ``` -### 以流的方式接收响应数据 +### 以流的方式接收响应数据: ```dart final rs = await dio.get( @@ -161,7 +161,7 @@ final rs = await dio.get( print(rs.data.stream); // 响应流 ``` -### 以二进制数组的方式接收响应数据 +### 以二进制数组的方式接收响应数据: ```dart final rs = await dio.get( @@ -171,7 +171,7 @@ final rs = await dio.get( print(rs.data); // 类型: List ``` -### 发送 `FormData` +### 发送 `FormData`: ```dart final formData = FormData.fromMap({ @@ -181,7 +181,7 @@ final formData = FormData.fromMap({ final response = await dio.post('/info', data: formData); ``` -### 通过 `FormData` 上传多个文件 +### 通过 `FormData` 上传多个文件: ```dart final formData = FormData.fromMap({ @@ -196,7 +196,7 @@ final formData = FormData.fromMap({ final response = await dio.post('/info', data: formData); ``` -### 监听发送(上传)数据进度 +### 监听发送(上传)数据进度: ```dart final response = await dio.post( @@ -208,7 +208,7 @@ final response = await dio.post( ); ``` -### 以流的形式提交二进制数据 +### 以流的形式提交二进制数据: ```dart // Binary data @@ -630,7 +630,7 @@ dio.post( ## 发送 FormData -Dio 支持发送 `FormData`, 请求数据将会以 `multipart/form-data` 方式编码, +Dio 支持发送 `FormData`, 请求数据将会以 `multipart/form-data` 方式编码, `FormData` 中可以包含一个或多个文件。 ```dart @@ -657,7 +657,7 @@ final formDataWithBoundaryName = FormData( ### 多文件上传 -多文件上传时,通过给 key 加中括号 `[]` 方式作为文件数组的标记,大多数后台也会通过 `key[]` 来读取多个文件。 +多文件上传时,通过给 key 加中括号 `[]` 方式作为文件数组的标记,大多数后台也会通过 `key[]` 来读取多个文件。 然而 RFC 标准中并没有规定多文件上传必须要使用 `[]`,关键在于后台与客户端之间保持一致。 ```dart @@ -692,7 +692,6 @@ formData.files.addAll([ 常见的错误做法是将 `FormData` 赋值给一个共享变量,在每次请求中都使用这个变量。 这样的操作会加大 **无法序列化** 的错误出现的可能性。 你可以像以下的代码一样编写你的请求以避免出现这样的错误: - ```dart Future _repeatedlyRequest() async { Future createFormData() async { @@ -760,16 +759,13 @@ dio.httpClientAdapter = HttpClientAdapter(); ``` 如果你需要单独使用对应平台的适配器: -* 对于 Web 平台 - +- 对于 Web 平台 ```dart import 'package:dio/browser.dart'; // ... dio.httpClientAdapter = BrowserHttpClientAdapter(); ``` - - 对于原生平台: - ```dart import 'package:dio/io.dart'; // ... @@ -919,7 +915,7 @@ token.cancel('cancelled'); ## 继承 Dio class `Dio` 是一个拥有工厂构造函数的接口类,因此不能直接继承 `Dio`, -但是可以继承 `DioForNative` 或 `DioForBrowser`: +但是可以继承 `DioForNative` 或 `DioForBrowser`: ```dart import 'package:dio/dio.dart'; From c550983822d115b485bd01f1cc23928cf1aac169 Mon Sep 17 00:00:00 2001 From: Seunghwan Lee Date: Tue, 24 Dec 2024 12:13:02 +0900 Subject: [PATCH 4/7] chore: IDE automation fixes --- dio/README.md | 40 ++++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/dio/README.md b/dio/README.md index 971e70428..b14bdd118 100644 --- a/dio/README.md +++ b/dio/README.md @@ -8,11 +8,11 @@ Language: English | [简体中文](README-ZH.md) A powerful HTTP networking package for Dart/Flutter, supports Global configuration, Interceptors, FormData, Request cancellation, File uploading/downloading, -Timeout, Custom adapters, Transformers, etc. +Timeout, Custom adapters, Transformers, etc. > Don't forget to add [#dio](https://pub.dev/packages?q=topic%3Adio) > topic to your published dio related packages! -> See more: +> See more: https://dart.dev/tools/pub/pubspec#topics
Table of content @@ -94,7 +94,7 @@ in [here](https://github.com/cfug/dio/issues/347). ## Examples -### Performing a `GET` request +### Performing a `GET` request: ```dart import 'package:dio/dio.dart'; @@ -114,19 +114,19 @@ void request() async { } ``` -### Performing a `POST` request +### Performing a `POST` request: ```dart response = await dio.post('/test', data: {'id': 12, 'name': 'dio'}); ``` -### Performing multiple concurrent requests +### Performing multiple concurrent requests: ```dart response = await Future.wait([dio.post('/info'), dio.get('/token')]); ``` -### Downloading a file +### Downloading a file: ```dart response = await dio.download( @@ -135,7 +135,7 @@ response = await dio.download( ); ``` -### Get response stream +### Get response stream: ```dart final rs = await dio.get( @@ -145,7 +145,7 @@ final rs = await dio.get( print(rs.data.stream); // Response stream. ``` -### Get response with bytes +### Get response with bytes: ```dart final rs = await Dio().get>( @@ -155,7 +155,7 @@ final rs = await Dio().get>( print(rs.data); // Type: List. ``` -### Sending a `FormData` +### Sending a `FormData`: ```dart final formData = FormData.fromMap({ @@ -165,7 +165,7 @@ final formData = FormData.fromMap({ final response = await dio.post('/info', data: formData); ``` -### Uploading multiple files to server by FormData +### Uploading multiple files to server by FormData: ```dart final formData = FormData.fromMap({ @@ -180,7 +180,7 @@ final formData = FormData.fromMap({ final response = await dio.post('/info', data: formData); ``` -### Listening the uploading progress +### Listening the uploading progress: ```dart final response = await dio.post( @@ -192,7 +192,7 @@ final response = await dio.post( ); ``` -### Post binary data with Stream +### Post binary data with Stream: ```dart // Binary data @@ -214,7 +214,7 @@ See all examples code [here](example). ## Dio APIs -### Creating an instance and set default configs +### Creating an instance and set default configs. > It is recommended to use a singleton of `Dio` in projects, which can manage configurations like headers, base urls, > and timeouts consistently. @@ -707,7 +707,6 @@ You should make a new `FormData` or `MultipartFile` every time in repeated reque A typical wrong behavior is setting the `FormData` as a variable and using it in every request. It can be easy for the *Cannot finalize* exceptions to occur. To avoid that, write your requests like the below code: - ```dart Future _repeatedlyRequest() async { Future createFormData() async { @@ -726,7 +725,7 @@ Future _repeatedlyRequest() async { `Transformer` allows changes to the request/response data before it is sent/received to/from the server. -Dio has already implemented a `BackgroundTransformer` as default, +Dio has already implemented a `BackgroundTransformer` as default, which calls `jsonDecode` in an isolate if the response is larger than 50 KB. If you want to customize the transformation of request/response data, you can provide a `Transformer` by your self, @@ -758,23 +757,20 @@ dio.httpClientAdapter = HttpClientAdapter(); ``` If you want to use platform adapters explicitly: -* For the Web platform: - +- For the Web platform: ```dart import 'package:dio/browser.dart'; // ... dio.httpClientAdapter = BrowserHttpClientAdapter(); ``` - - For native platforms: - ```dart import 'package:dio/io.dart'; // ... dio.httpClientAdapter = IOHttpClientAdapter(); ``` -[Here](../example/lib/adapter.dart) is a simple example to custom adapter. +[Here](../example/lib/adapter.dart) is a simple example to custom adapter. ### Using proxy @@ -814,7 +810,7 @@ the certificates protecting the TLS connection to the server are the ones you ex The intention is to reduce the chance of a man-in-the-middle attack. The theory is covered by [OWASP](https://owasp.org/www-community/controls/Certificate_and_Public_Key_Pinning). -*Server Response Certificate* +_Server Response Certificate_ Unlike other methods, this one works with the certificate of the server itself. @@ -853,7 +849,7 @@ openssl s_client -servername pinning-test.badssl.com -connect pinning-test.badss # (remove the formatting, keep only lower case hex characters to match the `sha256` above) ``` -*Certificate Authority Verification* +_Certificate Authority Verification_ These methods work well when your server has a self-signed certificate, but they don't work for certificates issued by a 3rd party like AWS or Let's Encrypt. From 9af8b9005b64c5158513eb9db1a69fa0440c8e88 Mon Sep 17 00:00:00 2001 From: seunghwanly Date: Wed, 25 Dec 2024 07:12:43 +0900 Subject: [PATCH 5/7] fix: close after used --- example_dart/lib/queued_interceptor_crsftoken.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/example_dart/lib/queued_interceptor_crsftoken.dart b/example_dart/lib/queued_interceptor_crsftoken.dart index 1a73a609a..7c6c3e1b5 100644 --- a/example_dart/lib/queued_interceptor_crsftoken.dart +++ b/example_dart/lib/queued_interceptor_crsftoken.dart @@ -73,12 +73,15 @@ void main() async { jsonEncode(AuthenticationServer.generate()).codeUnits, )}', ); + if (response.statusCode == null || response.statusCode! ~/ 100 != 2) { + tokenRefreshDio.close(); return handler.reject(error); } final body = jsonDecode(response.data) as Map; if (!body.containsKey('access_token')) { + tokenRefreshDio.close(); return handler.reject(error); } From 4ab72bfd663f11d684c5f404cadc59c27c3231c4 Mon Sep 17 00:00:00 2001 From: Seunghwan Lee Date: Tue, 31 Dec 2024 11:22:07 +0900 Subject: [PATCH 6/7] fix: close after used `tokenRefreshDio` closed in every condition but it could be closed right after request. --- example_dart/lib/queued_interceptor_crsftoken.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/example_dart/lib/queued_interceptor_crsftoken.dart b/example_dart/lib/queued_interceptor_crsftoken.dart index 7c6c3e1b5..ec740f0cc 100644 --- a/example_dart/lib/queued_interceptor_crsftoken.dart +++ b/example_dart/lib/queued_interceptor_crsftoken.dart @@ -73,21 +73,19 @@ void main() async { jsonEncode(AuthenticationServer.generate()).codeUnits, )}', ); + tokenRefreshDio.close(); if (response.statusCode == null || response.statusCode! ~/ 100 != 2) { - tokenRefreshDio.close(); return handler.reject(error); } final body = jsonDecode(response.data) as Map; if (!body.containsKey('access_token')) { - tokenRefreshDio.close(); return handler.reject(error); } final token = body['access_token'] as String; tokenManager.setAccessToken(token, error.requestOptions.hashCode); - tokenRefreshDio.close(); } /// Pretend authorization has been resolved and try again From 66bab273fba97e13857fd703cfe9fe49b7b87d6d Mon Sep 17 00:00:00 2001 From: Seunghwan Lee Date: Wed, 8 Jan 2025 09:15:11 +0900 Subject: [PATCH 7/7] chore: clear comments also, fixed the baseUrl + path combination more clearly, reflecting @AlexV525 review. baseUrl does not contain '/' and '/' is included starting from the path. L72 had a duplicated host, so I removed it. --- .../lib/queued_interceptor_crsftoken.dart | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/example_dart/lib/queued_interceptor_crsftoken.dart b/example_dart/lib/queued_interceptor_crsftoken.dart index ec740f0cc..e1cfd8175 100644 --- a/example_dart/lib/queued_interceptor_crsftoken.dart +++ b/example_dart/lib/queued_interceptor_crsftoken.dart @@ -8,7 +8,7 @@ void main() async { final dio = Dio( BaseOptions( - baseUrl: 'https://httpbun.com/', + baseUrl: 'https://httpbun.com', ), ); @@ -23,8 +23,8 @@ void main() async { ''', ); - /// In case, you have 'refresh_token' and needs to refresh your 'access_token' - /// Make a request for new 'access_token' and update from here + // 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'] = @@ -51,30 +51,29 @@ void main() async { ''', ); - /// This example only handles '401' status code, - /// The more complex scenario should handle more status codes e.g. '403', '404', etc. + // 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!); } - /// Consider [dio] can be requested in parallel. - /// - /// To prevent repeated requests to the 'Authentication Server' - /// to update our 'access_token', - /// we can compare with the previously requested 'access_token'. + // 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/'; + ..options.baseUrl = 'https://httpbun.com'; final response = await tokenRefreshDio.post( - 'https://httpbun.com/mix/s=201/b64=${base64.encode( + '/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); } @@ -88,7 +87,7 @@ void main() async { tokenManager.setAccessToken(token, error.requestOptions.hashCode); } - /// Pretend authorization has been resolved and try again + /// The authorization has been resolved so and try again with the request. final retried = await dio.fetch( error.requestOptions ..path = '/mix/s=200' @@ -97,6 +96,7 @@ void main() async { }, ); + // Treat codes other than 2XX as rejected. if (retried.statusCode == null || retried.statusCode! ~/ 100 != 2) { return handler.reject(error); }