diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 7852f13c6..95a87ab26 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -1,25 +1,19 @@ - - - - + + - + + + - - - + - - - - @@ -51,36 +45,36 @@ - - - - - + + + + + - - - - - + + + + + - - - - + + + + - - - - + + + + @@ -88,43 +82,43 @@ - + - - + + + + + - + - - + + - + - + - - - - - + + - - + + - - + + @@ -133,63 +127,68 @@ - + - - + + + + + - + - - + + + + + - - + + - - + + - + - + - - + + - + - - - - + + + + + + + - - - - - - + + @@ -203,36 +202,36 @@ - svg - header - onsend - onSend - onSu - _onSuccess - onerror - onsu - _checkIfNeedEnqueue - downlo - reject - min - mer _merg exec print check _executeInterceptors cookie - download validate raf _merge - request _mergeOptions _makeRequest - merge _makeFullPath _make merg + GETU + request + make + on TimeoutException + ma + _tr + maker + response + download + merge + _httpClient + _configHttpClient + close + DioError + makere + drain Response<T> @@ -287,27 +286,30 @@ @@ -319,10 +321,10 @@ DEFINITION_ORDER - @@ -354,13 +356,6 @@ - - - - - - - @@ -403,17 +398,7 @@ - - - - - + + + + + - - - + + + - + + + - - @@ -485,21 +482,7 @@ - - - - 1524332083042 - - - 1524332251422 - 1524332621783 @@ -830,58 +813,72 @@ - - - + + - - + + - - + + - - + + - - + + - - + + - - + + - + - - + + - - + + - - + - - - + + + - + @@ -911,7 +908,6 @@ - @@ -936,7 +932,8 @@ - @@ -963,7 +960,7 @@ file://$PROJECT_DIR$/lib/src/transformer.dart - 68 + 69 @@ -973,14 +970,9 @@ file://$PROJECT_DIR$/lib/src/dio.dart - 419 + 482 - - file://$PROJECT_DIR$/lib/src/dio.dart - 408 - file:///usr/local/opt/dart/libexec/lib/_http/http_headers.dart 989 @@ -993,7 +985,7 @@ file://$PROJECT_DIR$/test/dio_test.dart - 254 + 258 @@ -1006,20 +998,35 @@ 4 + + file://$PROJECT_DIR$/lib/src/interceptors/log.dart + 43 + + + file://$PROJECT_DIR$/test/dio_test.dart + 139 + file://$PROJECT_DIR$/lib/src/dio.dart - 698 - file://$PROJECT_DIR$/lib/src/dio.dart - 767 - - file://$PROJECT_DIR$/lib/src/interceptors/log.dart - 43 - + + file://$PROJECT_DIR$/lib/src/transformer.dart + 84 + @@ -1030,65 +1037,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1148,13 +1096,6 @@ - - - - - - - @@ -1190,70 +1131,73 @@ - - - - - - - - + - - + - + - + - - + + - + - - + + - + - + + - + - + - - + + + + + + + + + + + + + + + - + - - - - - + + - + - - + + @@ -1262,71 +1206,80 @@ - - + + - + - - - - - + + - + - - - - - + + - + - - + + - + - - + + - + - - - - + + - + - - + + + + + + + + + - + - + - - + + + + + + + + + + + + @@ -1335,111 +1288,152 @@ - - + + - + - - + + - + - - - - + + - + - - + + + + + + + + + + + + + + + + - + - + - - + + - + - - + + - + + + + - - + + + + + - + - - + + - + - + - - + + - + - + - - + + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - + - - + + - + diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d78052cb..bccfe5b33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ + +#2.0 + +**Refactor the Interceptors** +- Support add Multiple Interceptors. +- Add Log Interceptor +- Add CookieManager Interceptor + +**API** +- Support Uri +- Support `queryParameters` for all request API +- Modify the `get` API + +**Options** +- Separate Options to three class: Options、BaseOptions、RequestOptions +- Add `queryParameters` and `cookies` for BaseOptions + +**Adapter** +- Abstract HttpClientAdapter layer. +- Provide a DefaultHttpClientAdapter which make http requests by `dart:io:HttpClient` + ## 0.1.8 - change file name "TransFormer" to "Transformer" diff --git a/README-ZH.md b/README-ZH.md index 8b27cecb1..f8b5cc406 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -5,15 +5,17 @@ # dio -dio是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时等... +dio是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时、自定义适配器等... -### 添加依赖 +## 添加依赖 ```yaml dependencies: - dio: ^x.x.x // 请使用pub上的最新版本 + dio: ^2.0.x // 请使用pub上2.0分支的最新版本 ``` +如果您是1.0的用户,可以参照此文档升级到2.0,详情请查看 Change log 。 + ## 一个极简的示例 ```dart @@ -31,21 +33,7 @@ void getHttp() async { ## 内容列表 -- [示例](#示例) -- [Dio APIs](#dio-apis) -- [请求配置](#请求配置) -- [响应数据](#响应数据) -- [拦截器](#拦截器) -- [错误处理](#错误处理) -- [使用application/x-www-form-urlencoded编码](#使用applicationx-www-form-urlencoded编码) -- [FormData](#formdata) -- [转换器](#转换器) -- [设置Http代理](#设置Http代理) -- [Https证书校验](#Https证书校验) -- [请求取消](#请求取消) -- [Cookie管理](#cookie管理) -- [Features and bugs](#features-and-bugs) - +[TOC] ## 示例 @@ -53,31 +41,31 @@ void getHttp() async { 发起一个 `GET` 请求 : ```dart - Response response; - Dio dio = new Dio(); - response = await dio.get("/test?id=12&name=wendu") - print(response.data.toString()); +Response response; +Dio dio = new Dio(); +response = await dio.get("/test?id=12&name=wendu") +print(response.data.toString()); // 请求参数也可以通过对象传递,上面的代码等同于: - response = await dio.get("/test", data: {"id": 12, "name": "wendu"}); - print(response.data.toString()); +response = await dio.get("/test", queryParameters: {"id": '12', "name": "wendu"}); +print(response.data.toString()); ``` 发起一个 `POST` 请求: ```dart - response = await dio.post("/test", data: {"id": 12, "name": "wendu"}); +response = await dio.post("/test", data: {"id": 12, "name": "wendu"}); ``` 发起多个并发请求: ```dart - response = await Future.wait([dio.post("/info"), dio.get("/token")]); +response = await Future.wait([dio.post("/info"), dio.get("/token")]); ``` 下载文件: ```dart - response = await dio.download("https://www.google.com/", "./xx.html"); +response = await dio.download("https://www.google.com/", "./xx.html"); ``` 发送 FormData: @@ -87,13 +75,13 @@ FormData formData = new FormData.from({ "name": "wendux", "age": 25, }); - response = await dio.post("/info", data: formData); +response = await dio.post("/info", data: formData); ``` 通过FormData上传多个文件: ```dart - FormData formData = new FormData.from({ +FormData formData = new FormData.from({ "name": "wendux", "age": 25, "file1": new UploadFileInfo(new File("./upload.txt"), "upload1.txt"), @@ -102,11 +90,11 @@ FormData formData = new FormData.from({ utf8.encode("hello world"), "word.txt"), // 支持文件数组上传 "files": [ - new UploadFileInfo(new File("./example/upload.txt"), "upload.txt"), - new UploadFileInfo(new File("./example/upload.txt"), "upload.txt") + new UploadFileInfo(new File("./example/upload.txt"), "upload.txt"), + new UploadFileInfo(new File("./example/upload.txt"), "upload.txt") ] - }); - response = await dio.post("/info", data: formData); +}); +response = await dio.post("/info", data: formData); ``` 监听发送(上传)数据进度: @@ -127,53 +115,56 @@ response = await dio.post( ### 创建一个Dio实例,并配置它 -你可以使用默认配置或传递一个可选 `Options`参数来创建一个Dio实例 : +你可以使用默认配置或传递一个可选 `BaseOptions`参数来创建一个Dio实例 : ```dart Dio dio = new Dio; // 使用默认配置 // 配置dio实例 - dio.options.baseUrl = "https://www.xx.com/api"; - dio.options.connectTimeout = 5000; //5s - dio.options.receiveTimeout = 3000; +dio.options.baseUrl = "https://www.xx.com/api"; +dio.options.connectTimeout = 5000; //5s +dio.options.receiveTimeout = 3000; // 或者通过传递一个 `options`来创建dio实例 - Options options = new Options( - baseUrl: "https://www.xx.com/api", - connectTimeout: 5000, - receiveTimeout: 3000); - Dio dio = new Dio(options); +Options options = new BaseOptions( + baseUrl: "https://www.xx.com/api", + connectTimeout: 5000, + receiveTimeout: 3000, +); +Dio dio = new Dio(options); ``` Dio实例的核心API是 : -**Future request(String path, {data, Options options,CancelToken cancelToken})** +**Future request(String path, {data,Map queryParameters, Options options,CancelToken cancelToken})** ```dart response = await request( - "/test", data: {"id": 12, "name": "xx"}, new Options(method: "GET")); + "/test", + data: {"id": 12, "name": "xx"}, + new Options(method: "GET"), + ); ``` ### 请求方法别名 为了方便使用,Dio提供了一些其它的Restful API, 这些API都是`request`的别名。 -**Future get(path, {data, Options options,CancelToken cancelToken})** +**Future get(...)** -**Future post(path, {data, Options options,CancelToken cancelToken})** +**Future post(...)** -**Future put(path, {data, Options options,CancelToken cancelToken})** +**Future put(...)** -**Future delete(path, {data, Options options,CancelToken cancelToken})** +**Future delete(...)** -**Future head(path, {data, Options options,CancelToken cancelToken})** +**Future head(...)** -**Future put(path, {data, Options options,CancelToken cancelToken})** +**Future put(...)** -**Future path(path, {data, Options options,CancelToken cancelToken})** +**Future path(...)** -**Future download(String urlPath, savePath,** -​ **{OnDownloadProgress onProgress, data, bool flush: false, Options options,CancelToken cancelToken})** +**Future download(...)** ## 请求配置 @@ -227,6 +218,9 @@ Dio实例的核心API是 : /// 用户自定义字段,可以在 [Interceptor]、[Transformer] 和 [Response] 中取到. Map extra; + + /// 公共query参数 + Map*/ > queryParameters; } ``` @@ -263,34 +257,29 @@ Dio实例的核心API是 : ## 拦截器 -每一个 Dio 实例都有一个请求拦截器 `RequestInterceptor` 和一个响应拦截器 `ResponseInterceptor`, 通过拦截器你可以在请求之前或响应之后(但还没有被 `then` 或 `catchError`处理)做一些统一的预处理操作。 +每个 Dio 实例都可以添加任意多个拦截器,通过拦截器你可以在请求之前或响应之后(但还没有被 `then` 或 `catchError`处理)做一些统一的预处理操作。 ```dart - dio.interceptor.request.onSend = (Options options){ + +dio.interceptors.add(InterceptorsWrapper( + onRequest:(RequestOptions options){ // 在请求被发送之前做一些事情 return options; //continue // 如果你想完成请求并返回一些自定义数据,可以返回一个`Response`对象或返回`dio.resolve(data)`。 // 这样请求将会被终止,上层then会被调用,then中返回的数据将是你的自定义数据data. // // 如果你想终止请求并触发一个错误,你可以返回一个`DioError`对象,或返回`dio.reject(errMsg)`, - // 这样请求将被中止并触发异常,上层catchError会被调用。 - } - dio.interceptor.response.onSuccess = (Response response) { + // 这样请求将被中止并触发异常,上层catchError会被调用。 + }, + onResponse:(RequestOptions response) { // 在返回响应数据之前做一些预处理 return response; // continue - }; - dio.interceptor.response.onError = (DioError e){ - // 当请求失败时做一些预处理 + }, + onError: (DioError e) { + // 当请求失败时做一些预处理 return e;//continue - } -``` - -如果你想移除拦截器,你可以将它们置为null: - -```dart - dio.interceptor.request.onSend = null; - dio.interceptor.response.onSuccess = null; - dio.interceptor.response.onError = null; + } +)); ``` ### 完成和终止请求/响应 @@ -298,11 +287,13 @@ Dio实例的核心API是 : 在所有拦截器中,你都可以改变请求执行流, 如果你想完成请求/响应并返回自定义数据,你可以返回一个 `Response` 对象或返回 `dio.resolve(data)`的结果。 如果你想终止(触发一个错误,上层`catchError`会被调用)一个请求/响应,那么可以返回一个`DioError` 对象或返回 `dio.reject(errMsg)` 的结果. ```dart - dio.interceptor.request.onSend = (Options options) { - return dio.resolve("fake data") - } - Response response = await dio.get("/test"); - print(response.data); //"fake data" +dio.interceptors.add(InterceptorsWrapper( + onRequest:(RequestOptions options){ + return dio.resolve("fake data") + }, +)); +Response response = await dio.get("/test"); +print(response.data);//"fake data" ``` ### 拦截器中支持异步任务 @@ -310,13 +301,15 @@ Dio实例的核心API是 : 拦截器中不仅支持同步任务,而且也支持异步任务, 下面是在请求拦截器中发起异步任务的一个实例: ```dart - dio.interceptor.request.onSend = (Options options) async { - //...If no token, request token firstly. - Response response = await dio.get("/token"); - //Set the token to headers - options.headers["token"] = response.data["data"]["token"]; - return options; //continue - } +dio.interceptors.add(InterceptorsWrapper( + onRequest:(Options options) async{ + //...If no token, request token firstly. + Response response = await dio.get("/token"); + //Set the token to headers + options.headers["token"] = response.data["data"]["token"]; + return options; //continue + } +)); ``` ### Lock/unlock 拦截器 @@ -324,19 +317,21 @@ Dio实例的核心API是 : 你可以通过调用拦截器的 `lock()`/`unlock` 方法来锁定/解锁拦截器。一旦请求/响应拦截器被锁定,接下来的请求/响应将会在进入请求/响应拦截器之前排队等待,直到解锁后,这些入队的请求才会继续执行(进入拦截器)。这在一些需要串行化请求/响应的场景中非常实用,后面我们将给出一个示例。 ```dart - tokenDio = new Dio(); //Create a new instance to request the token. - tokenDio.options = dio; - dio.interceptor.request.onSend = (Options options) async { - // If no token, request token firstly and lock this interceptor - // to prevent other request enter this interceptor. - dio.interceptor.request.lock(); - // We use a new Dio(to avoid dead lock) instance to request token. - Response response = await tokenDio.get("/token"); - //Set the token to headers - options.headers["token"] = response.data["data"]["token"]; - dio.interceptor.request.unlock(); - return options; //continue - } +tokenDio = new Dio(); //Create a new instance to request the token. +tokenDio.options = dio; +dio.interceptors.add(InterceptorsWrapper( + onRequest:(Options options) async { + // If no token, request token firstly and lock this interceptor + // to prevent other request enter this interceptor. + dio.interceptors.requestLock.lock(); + // We use a new Dio(to avoid dead lock) instance to request token. + Response response = await tokenDio.get("/token"); + //Set the token to headers + options.headers["token"] = response.data["data"]["token"]; + dio.interceptors.requestLock.unlock(); + return options; //continue + } +)); ``` **Clear()** @@ -347,38 +342,70 @@ Dio实例的核心API是 : 当**请求**拦截器被锁定时,接下来的请求将会暂停,这等价于锁住了dio实例,因此,Dio示例上提供了**请求**拦截器`lock/unlock`的别名方法: -**dio.lock() == dio.interceptor.request.lock()** +**dio.lock() == dio.interceptors.requestLock.lock()** -**dio.unlock() == dio.interceptor.request.unlock()** +**dio.unlock() == dio.interceptors.requestLock.unlock()** -**dio.clear() == dio.interceptor.request.clear()** +**dio.clear() == dio.interceptors.requestLock.clear()** ### 示例 假设这么一个场景:出于安全原因,我们需要给所有的请求头中添加一个csrfToken,如果csrfToken不存在,我们先去请求csrfToken,获取到csrfToken后,再发起后续请求。 由于请求csrfToken的过程是异步的,我们需要在请求过程中锁定后续请求(因为它们需要csrfToken), 直到csrfToken请求成功后,再解锁,代码如下: ```dart -dio.interceptor.request.onSend = (Options options) { - print('send request:path:${options.path},baseURL:${options.baseUrl}'); - if (csrfToken == null) { - print("no token,request token firstly..."); - //lock the dio. - dio.lock(); - return tokenDio.get("/token").then((d) { - options.headers["csrfToken"] = csrfToken = d.data['data']['token']; - print("request token succeed, value: " + d.data['data']['token']); - print('continue to perform request:path:${options.path},baseURL:${options.path}'); - return options; - }).whenComplete(() => dio.unlock()); // unlock the dio - } else { - options.headers["csrfToken"] = csrfToken; - return options; +dio.interceptors.add(InterceptorsWrapper( + onRequest: (Options options) { + print('send request:path:${options.path},baseURL:${options.baseUrl}'); + if (csrfToken == null) { + print("no token,request token firstly..."); + //lock the dio. + dio.lock(); + return tokenDio.get("/token").then((d) { + options.headers["csrfToken"] = csrfToken = d.data['data']['token']; + print("request token succeed, value: " + d.data['data']['token']); + print( + 'continue to perform request:path:${options.path},baseURL:${options.path}'); + return options; + }).whenComplete(() => dio.unlock()); // unlock the dio + } else { + options.headers["csrfToken"] = csrfToken; + return options; + } } - }; +)); ``` 完整的示例代码请点击 [这里](https://github.com/flutterchina/dio/blob/flutter/example/interceptorLock.dart). +### 日志 + +我们可以添加 `LogInterceptor` 拦截器来自动打印请求、响应日志, 如: + +```dart +dio.interceptors.add(LogInterceptor(responseBody: false)); //开启请求日志 +``` + +### Cookie管理 + +我们可以通过添加`CookieManager`拦截器来自动管理请求/响应 cookie。`CookieManager` 依赖 `cookieJar` package: + +> dio cookie 管理 API 是基于开源库 [cookie_jar](https://github.com/flutterchina/cookie_jar). + +你可以创建一个`CookieJar` 或 `PersistCookieJar` 来帮您自动管理cookie, dio 默认使用 `CookieJar` , 它会将cookie保存在内存中。 如果您想对cookie进行持久化, 请使用 `PersistCookieJar` , 示例代码如下: + +```dart +var dio = new Dio(); +dio.interceptors.add(CookieManager(CookieJar())) +``` + +`PersistCookieJar` 实现了RFC中标准的cookie策略. `PersistCookieJar` 会将cookie保存在文件中,所以 cookies 会一直存在除非显式调用 `delete` 删除. + +更多关于 [cookie_jar](https://github.com/flutterchina/) 请参考 : https://github.com/flutterchina/cookie_jar . + +### 自定义拦截器 + +开发者可以通过继承`Interceptor` 类来实现自定义拦截器,这是一个简单的缓存示例拦截器。 + ## 错误处理 当请求过程中发生错误时, Dio 会包装 `Error/Exception` 为一个 `DioError`: @@ -490,37 +517,46 @@ response = await dio.post("/info", data: formData); 这是一个自定义转换器的[示例](https://github.com/wendux/dio/blob/flutter/example/Transformer.dart). -## 设置Http代理 +## HttpClientAdapter + +HttpClientAdapter是 Dio 和 HttpClient之间的桥梁。2.0抽象出adapter主要是方便切换、定制底层网络库。Dio实现了一套标准的、强大API,而HttpClient则是真正发起Http请求的对象。我们通过HttpClientAdapter将Dio和HttpClient解耦,这样一来便可以自由定制Http请求的底层实现,比如,在Flutter中我们可以通过自定义HttpClientAdapter将Http请求转发到Native中,然后再由Native统一发起请求。再比如,假如有一天OKHttp提供了dart版,你想使用OKHttp发起http请求,那么你便可以通过适配器来无缝切换到OKHttp,而不用改之前的代码。 + +Dio 使用`DefaultHttpClientAdapter`作为其默认HttpClientAdapter,`DefaultHttpClientAdapter`使用`dart:io:HttpClient` 来发起网络请求。 + -Dio 是使用 HttpClient发起的http请求,所以你可以通过配置 `httpClient`来支持代理,示例如下: + +### 设置Http代理 + +`DefaultHttpClientAdapter` 提供了一个`onHttpClientCreate` 回调来设置底层 `HttpClient`的代理,我们想使用代理,可以参考下面代码: ```dart - dio.onHttpClientCreate = (HttpClient client) { +(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) { + // config the http client client.findProxy = (uri) { - //proxy all request to localhost:8888 - return "PROXY localhost:8888"; + //proxy all request to localhost:8888 + return "PROXY localhost:8888"; }; - // 你也可以自己创建一个新的HttpClient实例返回。 - // return new HttpClient(SecurityContext); - }; + // you can also create a new HttpClient to dio + // return new HttpClient(); +}; ``` 完整的示例请查看[这里](https://github.com/wendux/dio/tree/flutter/example/proxy.dart). -## Https证书校验 +### Https证书校验 有两种方法可以校验https证书,假设我们的后台服务使用的是自签名证书,证书格式是PEM格式,我们将证书的内容保存在本地字符串中,那么我们的校验逻辑如下: ```dart - String PEM="XXXXX"; //证书内容 - dio.onHttpClientCreate = (HttpClient client) { +String PEM="XXXXX"; // certificate content +(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) { client.badCertificateCallback=(X509Certificate cert, String host, int port){ - if(cert.pem==PEM){ // 证书一致,则放行 - return true; - } - return false; + if(cert.pem==PEM){ // Verify the certificate + return true; + } + return false; }; - }; +}; ``` `X509Certificate`是证书的标准格式,包含了证书除私钥外所有信息,读者可以自行查阅文档。另外,上面的示例没有校验host,是因为只要服务器返回的证书内容和本地的保存一致就已经能证明是我们的服务器了(而不是中间人),host验证通常是为了防止证书和域名不匹配。 @@ -528,13 +564,13 @@ Dio 是使用 HttpClient发起的http请求,所以你可以通过配置 `httpC 对于自签名的证书,我们也可以将其添加到本地证书信任链中,这样证书验证时就会自动通过,而不会再走到`badCertificateCallback`回调中: ```dart - dio.onHttpClientCreate = (HttpClient client) { +(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) { SecurityContext sc = new SecurityContext(); - //file为证书路径 + //file is the path of certificate sc.setTrustedCertificates(file); HttpClient httpClient = new HttpClient(context: sc); return httpClient; - }; +}; ``` 注意,通过`setTrustedCertificates()`设置的证书格式必须为PEM或PKCS12,如果证书格式为PKCS12,则需将证书密码传入,这样则会在代码中暴露证书密码,所以客户端证书校验不建议使用PKCS12格式的证书。 @@ -561,23 +597,8 @@ token.cancel("cancelled"); 完整的示例请参考[取消示例](https://github.com/flutterchina/dio/blob/flutter/example/cancelRequest.dart). -## Cookie管理 - -你可以通过 `cookieJar` 来自动管理请求/响应cookie. - -> dio cookie 管理 API 是基于开源库 [cookie_jar](https://github.com/flutterchina/cookie_jar). - -你可以创建一个`CookieJar` 或 `PersistCookieJar` 来帮您自动管理cookie, dio 默认使用 `CookieJar` , 它会将cookie保存在内存中。 如果您想对cookie进行持久化, 请使用 `PersistCookieJar` , 示例代码如下: - -```dart -var dio = new Dio(); -dio.cookieJar = new PersistCookieJar("./cookies"); -``` - -`PersistCookieJar` 实现了RFC中标准的cookie策略. `PersistCookieJar` 会将cookie保存在文件中,所以 cookies 会一直存在除非显式调用 `delete` 删除. -更多关于 [cookie_jar](https://github.com/flutterchina/) 请参考 : https://github.com/flutterchina/cookie_jar . ## Copyright & License diff --git a/README.md b/README.md index 42f924f4c..8902cdf5b 100644 --- a/README.md +++ b/README.md @@ -68,13 +68,13 @@ void getHttp() async { Performing a `GET` request: ```dart - Response response; - Dio dio = new Dio(); - response = await dio.get("/test?id=12&name=wendu") - print(response.data.toString()); +Response response; +Dio dio = new Dio(); +response = await dio.get("/test?id=12&name=wendu") +print(response.data.toString()); // Optionally the request above could also be done as - response = await dio.get("/test", data: {"id": 12, "name": "wendu"}); - print(response.data.toString()); +response = await dio.get("/test", queryParameters: {"id": '12', "name": "wendu"}); +print(response.data.toString()); ``` Performing a `POST` request: @@ -86,7 +86,7 @@ response = await dio.post("/test", data: {"id": 12, "name": "wendu"}); Performing multiple concurrent requests: ```dart - response = await Future.wait([dio.post("/info"), dio.get("/token")]); +response = await Future.wait([dio.post("/info"), dio.get("/token")]); ``` Downloading a file: @@ -102,13 +102,13 @@ FormData formData = new FormData.from({ "name": "wendux", "age": 25, }); - response = await dio.post("/info", data: formData); +response = await dio.post("/info", data: formData); ``` Uploading multiple files to server by FormData: ```dart - FormData formData = new FormData.from({ +FormData formData = new FormData.from({ "name": "wendux", "age": 25, "file1": new UploadFileInfo(new File("./upload.txt"), "upload1.txt"), @@ -120,7 +120,7 @@ Uploading multiple files to server by FormData: new UploadFileInfo(new File("./example/upload.txt"), "upload.txt"), new UploadFileInfo(new File("./example/upload.txt"), "upload.txt") ] - }); +}); ``` Listening the uploading progress: @@ -141,57 +141,61 @@ response = await dio.post( ### Creating an instance and set default configs. -You can create instance of Dio with an optional `GlobalOptions` object: +You can create instance of Dio with an optional `BaseOptions` object: ```dart Dio dio = new Dio; // with default Options // Set default configs - dio.options.baseUrl = "https://www.xx.com/api"; - dio.options.connectTimeout = 5000; //5s - dio.options.receiveTimeout = 3000; +dio.options.baseUrl = "https://www.xx.com/api"; +dio.options.connectTimeout = 5000; //5s +dio.options.receiveTimeout = 3000; // or new Dio with a Options instance. - Options options = new GlobalOptions( - baseUrl: "https://www.xx.com/api", - connectTimeout: 5000, - receiveTimeout: 3000); - Dio dio = new Dio(options); +Options options = new BaseOptions( + baseUrl: "https://www.xx.com/api", + connectTimeout: 5000, + receiveTimeout: 3000, +); +Dio dio = new Dio(options); ``` The core API in Dio instance is: -**Future request(String path, {data, Options options,CancelToken cancelToken})** +**Future request(String path, {data,Map queryParameters, Options options,CancelToken cancelToken})** ```dart -response=await request("/test", data: {"id":12,"name":"xx"}, options: new Options(method:"GET")); +response=await request( + "/test", + data: {"id":12,"name":"xx"}, + options: new Options(method:"GET"), +); ``` ### Request method aliases For convenience aliases have been provided for all supported request methods. -**Future get(path, {data, Options options,CancelToken cancelToken})** +**Future get(...)** -**Future post(path, {data, Options options,CancelToken cancelToken})** +**Future post(...)** -**Future put(path, {data, Options options,CancelToken cancelToken})** +**Future put(...)** -**Future delete(path, {data, Options options,CancelToken cancelToken})** +**Future delete(...)** -**Future head(path, {data, Options options,CancelToken cancelToken})** +**Future head(...)** -**Future put(path, {data, Options options,CancelToken cancelToken})** +**Future put(...)** -**Future path(path, {data, Options options,CancelToken cancelToken})** +**Future path(...)** -**Future download(String urlPath, savePath,** -​ **{OnDownloadProgress onProgress, data, bool flush: false, Options options,CancelToken cancelToken})** +**Future download(...)** ## Request Options -These are the available config options for making requests. Request default `method` is `GET` if `method` is not specified. +The Options class describes the http request information and configuration. Each Dio instance has a base config for all requests maked by itself, and we can override the base config with [Options] when make a single request. The [BaseOptions] declaration as follows: ```dart { @@ -242,11 +246,8 @@ These are the available config options for making requests. Request default `met /// the request will be perceived as successful; otherwise, considered as failed. ValidateStatus validateStatus; - /// Custom field that you can retrieve it later in [Interceptor]、[Transformer] and the [Response] object. + /// Custom field that you can retrieve it later in [Interceptor]、[Transformer] and the [Response] object. Map extra; - - /// Full path. - Uri uri; /// Custom Cookies Iterable cookies; @@ -286,33 +287,28 @@ print(response.statusCode); ## Interceptors -Each Dio instance has a `RequestInterceptor` and a `ResponseInterceptor`, by which you can intercept requests or responses before they are handled by `then` or `catchError`. +For each dio instance, We can add one or more interceptors, by which we can intercept requests or responses before they are handled by `then` or `catchError`. ```dart - dio.interceptor.request.onSend = (Options options){ +dio.interceptors.add(InterceptorsWrapper( + onRequest:(RequestOptions options){ // Do something before request is sent return options; //continue // If you want to resolve the request with some custom data, // you can return a `Response` object or return `dio.resolve(data)`. // If you want to reject the request with a error message, // you can return a `DioError` object or return `dio.reject(errMsg)` - }; - dio.interceptor.response.onSuccess = (Response response) { + }, + onResponse:(RequestOptions response) { // Do something with response data return response; // continue - }; - dio.interceptor.response.onError = (DioError e) { + }, + onError: (DioError e) { // Do something with response error return e;//continue - }; -``` - -If you may need to remove an interceptor later you can. + } +)); -```dart -dio.interceptor.request.onSend = null; -dio.interceptor.response.onSuccess = null; -dio.interceptor.response.onError = null; ``` ### Resolve and reject the request @@ -320,11 +316,13 @@ dio.interceptor.response.onError = null; In all interceptors, you can interfere with their execution flow. If you want to resolve the request/response with some custom data,you can return a `Response` object or return `dio.resolve(data)`. If you want to reject the request/response with a error message, you can return a `DioError` object or return `dio.reject(errMsg)` . ```dart - dio.interceptor.request.onSend = (Options options){ - return dio.resolve("fake data") - } - Response response = await dio.get("/test"); - print(response.data);//"fake data" +dio.interceptors.add(InterceptorsWrapper( + onRequest:(RequestOptions options){ + return dio.resolve("fake data") + }, +)); +Response response = await dio.get("/test"); +print(response.data);//"fake data" ``` ### Supports Async tasks in Interceptors @@ -332,15 +330,15 @@ In all interceptors, you can interfere with their execution flow. If you want to Interceptors not only support synchronous tasks, but also supports asynchronous tasks, for example: ```dart - dio.interceptors.add(InterceptorsWrapper( +dio.interceptors.add(InterceptorsWrapper( onRequest:(Options options) async{ - //...If no token, request token firstly. - Response response = await dio.get("/token"); - //Set the token to headers - options.headers["token"] = response.data["data"]["token"]; - return options; //continue + //...If no token, request token firstly. + Response response = await dio.get("/token"); + //Set the token to headers + options.headers["token"] = response.data["data"]["token"]; + return options; //continue } - )); +)); ``` ### Lock/unlock the interceptors @@ -348,20 +346,21 @@ Interceptors not only support synchronous tasks, but also supports asynchronous You can lock/unlock the interceptors by calling their `lock()`/`unlock` method. Once the request/response interceptor is locked, the incoming request/response will be added to a queue before they enter the interceptor, they will not be continued until the interceptor is unlocked. ```dart - tokenDio = new Dio(); //Create a new instance to request the token. - tokenDio.options = dio; - dio.interceptors.add(InterceptorsWrapper( - onRequest:(Options options) async { - // If no token, request token firstly and lock this interceptor - // to prevent other request enter this interceptor. - dio.interceptors.requestLock.lock(); - // We use a new Dio(to avoid dead lock) instance to request token. - Response response = await tokenDio.get("/token"); - //Set the token to headers - options.headers["token"] = response.data["data"]["token"]; - dio.interceptors.requestLock.unlock(); - return options; //continue - })); +tokenDio = new Dio(); //Create a new instance to request the token. +tokenDio.options = dio; +dio.interceptors.add(InterceptorsWrapper( + onRequest:(Options options) async { + // If no token, request token firstly and lock this interceptor + // to prevent other request enter this interceptor. + dio.interceptors.requestLock.lock(); + // We use a new Dio(to avoid dead lock) instance to request token. + Response response = await tokenDio.get("/token"); + //Set the token to headers + options.headers["token"] = response.data["data"]["token"]; + dio.interceptors.requestLock.unlock(); + return options; //continue + } +)); ``` You can clean the waiting queue by calling `clear()`; @@ -374,9 +373,7 @@ When the **request** interceptor is locked, the incoming request will pause, thi **dio.unlock() == dio.interceptors.requestLock.unlock()** - - - +**dio.clear() == dio.interceptors.requestLock.clear()** ### Example @@ -415,31 +412,48 @@ You can set `LogInterceptor` to print request/response log automaticlly, for e dio.interceptors.add(LogInterceptor(responseBody: false)); //开启请求日志 ``` +### Cookie Manager + +CookieManager Interceptor can help us manage the request/response cookies automaticly. CookieManager depends on `cookieJar` package : + +> The dio cookie manage API is based on the withdrawn [cookie_jar](https://github.com/flutterchina/cookie_jar). + +You can create a `CookieJar` or `PersistCookieJar` to manage cookies automatically, and dio use the `CookieJar` by default, which saves the cookies **in RAM**. If you want to persists cookies, you can use the `PersistCookieJar` class, the example codes as follows: + +```dart +var dio = new Dio(); +dio.interceptors.add(CookieManager(CookieJar())) +``` + +`PersistCookieJar` is a cookie manager which implements the standard cookie policy declared in RFC. `PersistCookieJar` persists the cookies in files, so if the application exit, the cookies always exist unless call `delete` explicitly. + +More details about [cookie_jar](https://github.com/flutterchina/cookie_jar) see : https://github.com/flutterchina/cookie_jar . + ### Custom Interceptor -You can custom interceptor by extendding the `Interceptor` class. There is an example that implementing a simple cache policy: custom cache interceptor +You can custom interceptor by extending the `Interceptor` class. There is an example that implementing a simple cache policy: custom cache interceptor. ## Handling Errors When a error occurs, Dio will wrap the `Error/Exception` to a `DioError`: ```dart - try { +try { //404 await dio.get("https://wendux.github.io/xsddddd"); - } on DioError catch(e) { - // The request was made and the server responded with a status code - // that falls out of the range of 2xx and is also not 304. - if(e.response) { +} on DioError catch(e) { + // The request was made and the server responded with a status code + // that falls out of the range of 2xx and is also not 304. + if(e.response) { print(e.response.data) print(e.response.headers) print(e.response.request) - } else{ + } else{ // Something happened in setting up or sending the request that triggered an Error print(e.request) print(e.message) - } - } + } +} ``` ### DioError scheme @@ -522,50 +536,66 @@ There is a complete example [here](https://github.com/flutterchina/dio/tree/flut There is an example for [customizing Transformer](https://github.com/flutterchina/dio/blob/flutter/example/Transformer.dart). -## Set proxy and HttpClient config +## Adapter + +HttpClientAdapter is a bridge between Dio and HttpClient. + +Dio implements standard and friendly API for developer. + +HttpClient: It is the real object that makes Http requests. + +We can use any HttpClient not just `dart:io:HttpClient` to make the Http request. And all we need is providing a `HttpClientAdapter`. The default HttpClientAdapter for Dio is `DefaultHttpClientAdapter`. + +```dart +dio.httpClientAdapter = new DefaultHttpClientAdapter(); +``` + + + +### Using proxy -Dio uses HttpClient to send http request, so you can config the `dio.httpClient` to support `proxy`, for example: +`DefaultHttpClientAdapter` provide a callback to set proxy to `dart:io:HttpClient`, for example: ```dart - dio.onHttpClientCreate = (HttpClient client) { +(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) { // config the http client client.findProxy = (uri) { - //proxy all request to localhost:8888 - return "PROXY localhost:8888"; + //proxy all request to localhost:8888 + return "PROXY localhost:8888"; }; // you can also create a new HttpClient to dio // return new HttpClient(); - }; +}; ``` There is a complete example [here](https://github.com/flutterchina/dio/tree/flutter/example/proxy.dart). -## Https certificate verification +### Https certificate verification There are two ways to verify the https certificate. Suppose the certificate format is PEM, the code like: ```dart - String PEM="XXXXX"; // certificate content - dio.onHttpClientCreate = (HttpClient client) { +String PEM="XXXXX"; // certificate content +(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) { client.badCertificateCallback=(X509Certificate cert, String host, int port){ - if(cert.pem==PEM){ // Verify the certificate - return true; - } - return false; + if(cert.pem==PEM){ // Verify the certificate + return true; + } + return false; }; - }; +}; ``` Another way is creating a `SecurityContext` when create the `HttpClient`: ```dart - dio.onHttpClientCreate = (HttpClient client) { +(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) { SecurityContext sc = new SecurityContext(); //file is the path of certificate sc.setTrustedCertificates(file); HttpClient httpClient = new HttpClient(context: sc); return httpClient; - }; +}; ``` In this way, the format of certificate must be PEM or PKCS12. @@ -585,24 +615,6 @@ token.cancel("cancelled"); There is a complete example [here](https://github.com/flutterchina/dio/tree/flutter/example/cancelRequest.dart). -## Cookie Manager - -You can manage the request/response cookies using `cookieJar` . - -> The dio cookie manage API is based on the withdrawn [cookie_jar](https://github.com/flutterchina/cookie_jar). - -You can create a `CookieJar` or `PersistCookieJar` to manage cookies automatically, and dio use the `CookieJar` by default, which saves the cookies **in RAM**. If you want to persists cookies, you can use the `PersistCookieJar` class, the example codes as follows: - -```dart -var dio = new Dio(); -dio.cookieJar=new PersistCookieJar("./cookies"); -``` - -`PersistCookieJar` is a cookie manager which implements the standard cookie policy declared in RFC. `PersistCookieJar` persists the cookies in files, so if the application exit, the cookies always exist unless call `delete` explicitly. - - -More details about [cookie_jar](https://github.com/flutterchina/cookie_jar) see : https://github.com/flutterchina/cookie_jar . - ## Copyright & License This open source project authorized by https://flutterchina.club , and the license is MIT. diff --git a/example/download.dart b/example/download.dart index c960acd08..e037de55a 100644 --- a/example/download.dart +++ b/example/download.dart @@ -5,7 +5,8 @@ import 'package:dio/dio.dart'; main() async { var dio = new Dio(); - dio.onHttpClientCreate=(HttpClient client){ + (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = + (HttpClient client) { client.idleTimeout = new Duration(seconds: 0); }; @@ -13,19 +14,25 @@ main() async { // var url = "http://download.dcloud.net.cn/HBuilder.9.0.2.macosx_64.dmg"; // This is a image, about 4KB - //var url = "https://flutter.io/assets/flutter-lockup-4cb0ee072ab312e59784d9fbf4fb7ad42688a7fdaea1270ccf6bbf4f34b7e03f.svg"; - var url="https://github.com/wendux/tt"; + var url = "https://flutter.io/assets/flutter-lockup-4cb0ee072ab312e59784d9fbf4fb7ad42688a7fdaea1270ccf6bbf4f34b7e03f.svg"; + //var url = "https://github.com/wendux/tt"; //404 try { - Response response=await dio.download(url, + Response response = await dio.download( + url, "./example/flutter.svg", onProgress: (received, total) { - print((received / total * 100).toStringAsFixed(0) + "%"); + if (total != -1) { + print((received / total * 100).toStringAsFixed(0) + "%"); + } }, - options: Options(receiveDataWhenStatusError: false) + cancelToken: CancelToken(), + options: Options( + //receiveDataWhenStatusError: false, + headers: {HttpHeaders.acceptEncodingHeader: "*"}, + ), ); - print(response.statusCode); - } on DioError catch (e) { + print("download succeed!"); + } catch (e) { print(e.response.data); } - print("download succeed!"); } diff --git a/example/proxy.dart b/example/proxy.dart index 787fc2de6..0a53a3d55 100644 --- a/example/proxy.dart +++ b/example/proxy.dart @@ -5,7 +5,7 @@ main() async { var dio = new Dio(); //dio.options.connectTimeout = 2000; // More about HttpClient proxy topic please refer to Dart SDK doc. - dio.onHttpClientCreate = (HttpClient client) { + (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (HttpClient client) { client.findProxy = (uri) { //proxy all request to localhost:8888 return "PROXY localhost:8888"; @@ -19,4 +19,4 @@ main() async { //print(response.data); response = await dio.get("https://www.baidu.com"); //print(response.data); -} \ No newline at end of file +} diff --git a/example/test.dart b/example/test.dart index 321f29588..aacfaae58 100644 --- a/example/test.dart +++ b/example/test.dart @@ -4,5 +4,5 @@ import 'package:dio/dio.dart'; main() async { var dio = new Dio(); dio.interceptors.add(LogInterceptor(responseBody: false)); - dio.get("https://github.com/wendux/tt?aa=b",queryParameters: {"kk":"tt"}); + dio.get("https://github.com/wendux/tt?aa=b",queryParameters: {"kk":"tt"}).catchError(print); } diff --git a/example/transfomer.dart b/example/transfomer.dart index 1bf7cfb7e..ecd4b4b34 100644 --- a/example/transfomer.dart +++ b/example/transfomer.dart @@ -11,7 +11,7 @@ class MyTransformer extends DefaultTransformer { @override Future transformRequest(RequestOptions options) async { - if (options.data is List) { + if (options.data is List) { throw new DioError(message: "Can't send List to sever directly"); } else { return super.transformRequest(options); @@ -22,8 +22,8 @@ class MyTransformer extends DefaultTransformer { /// info to [Options.extra], and you can retrieve it in [ResponseInterceptor] /// and [Response] with `response.request.extra["cookies"]`. @override - Future transformResponse(RequestOptions options, HttpClientResponse response) async { - options.extra["cookies"] = response.cookies; + Future transformResponse(RequestOptions options, ResponseBody response) async { + options.extra["self"] = 'XX'; return super.transformResponse(options, response); } @@ -34,11 +34,11 @@ main() async { // Use custom Transformer dio.transformer = new MyTransformer(); -// Response response = await dio.get("https://www.baidu.com"); -// print(response.request.extra["cookies"]); + Response response = await dio.get("https://www.baidu.com"); + print(response.request.extra["self"]); try { - await dio.post("https://www.baidu.com", data: [1, 2]); + await dio.post("https://www.baidu.com", data: ["1", "2"]); } catch (e) { print(e); } diff --git a/lib/dio.dart b/lib/dio.dart index 2fb72105c..fc1cf13ab 100644 --- a/lib/dio.dart +++ b/lib/dio.dart @@ -16,3 +16,4 @@ export 'src/cancel_token.dart'; export 'src/upload_file_info.dart'; export 'src/interceptors/log.dart'; export 'src/interceptors/cookie_mgr.dart'; +export 'src/adapter.dart'; diff --git a/lib/src/adapter.dart b/lib/src/adapter.dart new file mode 100644 index 000000000..391b36f71 --- /dev/null +++ b/lib/src/adapter.dart @@ -0,0 +1,136 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:async'; +import 'options.dart'; +import 'dio_error.dart'; + +typedef CancelWrapper = Future Function(Future); +typedef OnHttpClientCreate = dynamic Function(HttpClient client); +typedef VoidCallback = dynamic Function(); + +/// HttpAdapter is a bridge between Dio and HttpClient. +/// +/// Dio: Implements standard and friendly API for developer. +/// +/// HttpClient: It is the real object that makes Http +/// requests. +/// +/// We can use any HttpClient not just "dart:io:HttpClient" to +/// make the Http request. All we need is providing a [HttpClientAdapter]. +/// +/// The default HttpClientAdapter for Dio is [DefaultHttpClientAdapter]. +/// +/// ```dart +/// dio.httpClientAdapter = new DefaultHttpClientAdapter(); +/// ``` +abstract class HttpClientAdapter { + Future sendRequest( + RequestOptions options, + Stream> requestStream, + CancelWrapper cancelWrapper, + Future cancelFuture, + ); +} + +class ResponseBody { + ResponseBody(this.stream, this.statusCode, this.headers, + {VoidCallback onClose}) + : _onClose = onClose; + + Stream> stream; + HttpHeaders headers; + int statusCode; + VoidCallback _onClose; + + ResponseBody.fromString(String text, this.statusCode, this.headers, + {VoidCallback onClose}) + : _onClose = onClose, + stream = + Stream.fromIterable(utf8.encode(text).map((e) => [e]).toList()); + + ResponseBody.fromBytes(List bytes, this.statusCode, this.headers, + {VoidCallback onClose}) + : _onClose = onClose, + stream = Stream.fromIterable(bytes.map((e) => [e]).toList()); + + void close() { + if (_onClose != null) _onClose(); + } +} + +/// The default HttpClientAdapter for Dio is [DefaultHttpClientAdapter]. +class DefaultHttpClientAdapter extends HttpClientAdapter { + HttpClient _httpClient; + + Future sendRequest( + RequestOptions options, + Stream> requestStream, + CancelWrapper cancelWrapper, + Future cancelFuture, + ) async { + if (_httpClient == null) { + _httpClient = _configHttpClient(new HttpClient(), true); + } + var httpClient = _httpClient; + if (cancelFuture != null) { + httpClient = _configHttpClient(new HttpClient()); + //if request was cancelled , close httpClient + cancelFuture.then((e) => httpClient.close(force: true)); + } + Future requestFuture; + if (options.connectTimeout > 0) { + requestFuture = httpClient + .openUrl(options.method, options.uri) + .timeout(new Duration(milliseconds: options.connectTimeout)); + } else { + requestFuture = httpClient.openUrl(options.method, options.uri); + } + + HttpClientRequest request; + try { + request = await cancelWrapper(requestFuture); + //Set Headers + options.headers.forEach((k, v) => request.headers.set(k, v)); + } on TimeoutException { + throw new DioError( + request: options, + message: "Connecting timeout[${options.connectTimeout}ms]", + type: DioErrorType.CONNECT_TIMEOUT, + ); + } + request.followRedirects = options.followRedirects; + + try { + if (options.method != "GET" && requestStream != null) { + // Transform the request data, set headers inner. + await request.addStream(requestStream); + } + } catch (e) { + //If user cancel the request in transformer, close the connect by hand. + request.addError(e); + } + HttpClientResponse responseStream = await cancelWrapper(request.close()); + return new ResponseBody( + responseStream, + responseStream.statusCode, + responseStream.headers, + onClose: + cancelFuture != null ? () => httpClient.close(force: true) : null, + ); + } + + HttpClient _configHttpClient(HttpClient httpClient, + [bool isDefault = false]) { + httpClient.idleTimeout = new Duration(seconds: isDefault ? 3 : 0); + if (onHttpClientCreate != null) { + //user can return a new HttpClient instance + httpClient = onHttpClientCreate(httpClient) ?? httpClient; + } + return httpClient; + } + + /// [Dio] will create new HttpClient when it is needed. + /// If [onHttpClientCreate] is provided, [Dio] will call + /// it when a new HttpClient created. + OnHttpClientCreate onHttpClientCreate; +} diff --git a/lib/src/dio.dart b/lib/src/dio.dart index 82e7a0a73..863e49fcf 100644 --- a/lib/src/dio.dart +++ b/lib/src/dio.dart @@ -9,6 +9,7 @@ import 'interceptor.dart'; import 'options.dart'; import 'response.dart'; import 'transformer.dart'; +import 'adapter.dart'; /// Callback to listen the file downloading progress. /// @@ -26,8 +27,6 @@ typedef void OnDownloadProgress(int received, int total); /// [total] is the content length of the post body. typedef OnUploadProgress(int sent, int total); -typedef dynamic OnHttpClientCreate(HttpClient client); - /// A powerful Http client for Dart, which supports Interceptors, /// Global configuration, FormData, File downloading etc. and Dio is /// very easy to use. @@ -47,14 +46,6 @@ class Dio { /// Default Request config. More see [BaseOptions] . BaseOptions options; - /// [Dio] will create new HttpClient when it is needed. - /// If [onHttpClientCreate] is provided, [Dio] will call - /// it when a new HttpClient created. - OnHttpClientCreate onHttpClientCreate; - - bool _httpClientInited = false; - HttpClient _httpClient = new HttpClient(); - /// Each Dio instance has a interceptor by which you can intercept requests or responses before they are /// handled by `then` or `catchError`. the [interceptor] field /// contains a [RequestInterceptor] and a [ResponseInterceptor] instance. @@ -64,6 +55,14 @@ class Dio { Interceptors get interceptors => _interceptors; + HttpClientAdapter _httpClientAdapter = new DefaultHttpClientAdapter(); + + HttpClientAdapter get httpClientAdapter => _httpClientAdapter; + + set httpClientAdapter(HttpClientAdapter adapter) { + if (adapter != null) _httpClientAdapter = adapter; + } + /// [transformer] allows changes to the request/response data before it is sent/received to/from the server /// This is only applicable for request methods 'PUT', 'POST', and 'PATCH'. Transformer transformer = new DefaultTransformer(); @@ -100,6 +99,7 @@ class Dio { Future> post( String path, { data, + Map*/ > queryParameters, Options options, CancelToken cancelToken, OnUploadProgress onUploadProgress, @@ -107,6 +107,7 @@ class Dio { return request(path, data: data, options: _checkOptions("POST", options), + queryParameters: queryParameters, cancelToken: cancelToken, onUploadProgress: onUploadProgress); } @@ -129,13 +130,18 @@ class Dio { /// Handy method to make http PUT request, which is a alias of [Dio.request]. Future> put(String path, {data, + Map*/ > queryParameters, Options options, CancelToken cancelToken, OnUploadProgress onUploadProgress}) { - return request(path, - data: data, - options: _checkOptions("PUT", options), - cancelToken: cancelToken); + return request( + path, + data: data, + queryParameters: queryParameters, + options: _checkOptions("PUT", options), + cancelToken: cancelToken, + onUploadProgress: onUploadProgress, + ); } /// Handy method to make http PUT request, which is a alias of [Dio.request]. @@ -146,24 +152,39 @@ class Dio { CancelToken cancelToken, OnUploadProgress onUploadProgress, }) { - return requestUri(uri, - data: data, - options: _checkOptions("PUT", options), - cancelToken: cancelToken); + return requestUri( + uri, + data: data, + options: _checkOptions("PUT", options), + cancelToken: cancelToken, + onUploadProgress: onUploadProgress, + ); } /// Handy method to make http HEAD request, which is a alias of [Dio.request]. - Future> head(String path, - {data, Options options, CancelToken cancelToken}) { - return request(path, - data: data, - options: _checkOptions("HEAD", options), - cancelToken: cancelToken); + Future> head( + String path, { + data, + Map*/ > queryParameters, + Options options, + CancelToken cancelToken, + }) { + return request( + path, + data: data, + queryParameters: queryParameters, + options: _checkOptions("HEAD", options), + cancelToken: cancelToken, + ); } /// Handy method to make http HEAD request, which is a alias of [Dio.request]. - Future> headUri(Uri uri, - {data, Options options, CancelToken cancelToken}) { + Future> headUri( + Uri uri, { + data, + Options options, + CancelToken cancelToken, + }) { return requestUri(uri, data: data, options: _checkOptions("HEAD", options), @@ -171,39 +192,67 @@ class Dio { } /// Handy method to make http DELETE request, which is a alias of [Dio.request]. - Future> delete(String path, - {data, Options options, CancelToken cancelToken}) { - return request(path, - data: data, - options: _checkOptions("DELETE", options), - cancelToken: cancelToken); + Future> delete( + String path, { + data, + Map*/ > queryParameters, + Options options, + CancelToken cancelToken, + }) { + return request( + path, + data: data, + queryParameters: queryParameters, + options: _checkOptions("DELETE", options), + cancelToken: cancelToken, + ); } /// Handy method to make http DELETE request, which is a alias of [Dio.request]. - Future> deleteUri(Uri uri, - {data, Options options, CancelToken cancelToken}) { - return requestUri(uri, - data: data, - options: _checkOptions("DELETE", options), - cancelToken: cancelToken); + Future> deleteUri( + Uri uri, { + data, + Options options, + CancelToken cancelToken, + }) { + return requestUri( + uri, + data: data, + options: _checkOptions("DELETE", options), + cancelToken: cancelToken, + ); } /// Handy method to make http PATCH request, which is a alias of [Dio.request]. - Future> patch(String path, - {data, Options options, CancelToken cancelToken}) { - return request(path, - data: data, - options: _checkOptions("PATCH", options), - cancelToken: cancelToken); + Future> patch( + String path, { + data, + Map*/ > queryParameters, + Options options, + CancelToken cancelToken, + }) { + return request( + path, + data: data, + queryParameters: queryParameters, + options: _checkOptions("PATCH", options), + cancelToken: cancelToken, + ); } /// Handy method to make http PATCH request, which is a alias of [Dio.request]. - Future> patchUri(Uri uri, - {data, Options options, CancelToken cancelToken}) { - return requestUri(uri, - data: data, - options: _checkOptions("PATCH", options), - cancelToken: cancelToken); + Future> patchUri( + Uri uri, { + data, + Options options, + CancelToken cancelToken, + }) { + return requestUri( + uri, + data: data, + options: _checkOptions("PATCH", options), + cancelToken: cancelToken, + ); } /// Assure the final future state is succeed! @@ -291,6 +340,7 @@ class Dio { String urlPath, savePath, { OnDownloadProgress onProgress, + Map*/ > queryParameters, CancelToken cancelToken, lengthHeader: HttpHeaders.contentLengthHeader, data, @@ -299,33 +349,34 @@ class Dio { // We set the `responseType` to [ResponseType.STREAM] to retrieve the // response stream. if (options != null) { - options.method=options.method ?? "GET"; + options.method = options.method ?? "GET"; } else { options = _checkOptions("GET", options); } - HttpClient httpClient = new HttpClient(); - httpClient = _configHttpClient(httpClient); - // Receive data with stream. options.responseType = ResponseType.stream; - Response response; + Response response; try { response = await _request( urlPath, data: data, options: options, - cancelToken: cancelToken, - httpClient: httpClient, + queryParameters: queryParameters, + cancelToken: cancelToken ?? CancelToken(), ); } on DioError catch (e) { - if (e.type == DioErrorType.RESPONSE && - options.receiveDataWhenStatusError) { - var res = await transformer.transformResponse( - e.response.request.merge(responseType: ResponseType.json), - e.response.data, - ); - e.response.data = res; + if (e.type == DioErrorType.RESPONSE) { + if (options.receiveDataWhenStatusError) { + var res = await transformer.transformResponse( + e.response.request..responseType = ResponseType.json, + e.response.data, + ); + e.response.data = res; + } else { + e.response.data.close(); + e.response.data = null; + } } rethrow; } @@ -343,10 +394,10 @@ class Dio { int received = 0; // Stream> - Stream> stream = response.data; + Stream> stream = response.data.stream; // Handle timeout if (options.receiveTimeout > 0) { - stream = response.data.timeout( + stream = stream.timeout( new Duration(milliseconds: options.receiveTimeout), onTimeout: (EventSink sink) { sink.addError(new DioError( @@ -375,7 +426,7 @@ class Dio { (data) { // Check if cancelled. if (cancelToken != null && cancelToken.cancelError != null) { - httpClient.close(force: true); + response.data.close(); return; } // Write file. @@ -464,19 +515,11 @@ class Dio { Options options, OnUploadProgress onUploadProgress, }) async { - var httpClient = _httpClient; - if (cancelToken != null) { - httpClient = _configHttpClient(new HttpClient()); - } else if (!_httpClientInited) { - _httpClient = httpClient = _configHttpClient(_httpClient, true); - _httpClientInited = true; - } return _request(path, data: data, queryParameters: queryParameters, cancelToken: cancelToken, options: options, - httpClient: httpClient, onUploadProgress: onUploadProgress); } @@ -501,16 +544,6 @@ class Dio { onUploadProgress: onUploadProgress); } - HttpClient _configHttpClient(HttpClient httpClient, - [bool isDefault = false]) { - httpClient.idleTimeout = new Duration(seconds: isDefault ? 3 : 0); - if (onHttpClientCreate != null) { - //user can return a new HttpClient instance - httpClient = onHttpClientCreate(httpClient) ?? httpClient; - } - return httpClient; - } - Future _assureFuture(e) { if (e is! Future) { return Future.value(e); @@ -539,9 +572,12 @@ class Dio { Map queryParameters, CancelToken cancelToken, Options options, - HttpClient httpClient, OnUploadProgress onUploadProgress, }) async { + Completer cancelCompleter; + if (cancelToken != null) { + cancelCompleter = Completer(); + } RequestOptions requestOptions = _mergeOptions(options, path, data, queryParameters); Future> future = @@ -553,8 +589,8 @@ class Dio { // If the Future value type is Options, continue the network request. if (data is RequestOptions) { requestOptions.method = data.method.toUpperCase(); - response = - _makeRequest(data, cancelToken, httpClient, onUploadProgress); + response = _makeRequest( + data, cancelToken, cancelCompleter, onUploadProgress); } else { // Otherwise, use the Future value as the request result. // If the return type is Error, we should throw it @@ -565,68 +601,34 @@ class Dio { }).catchError((err) => throw _assureDioError(err)); }); - return await _listenCancelForAsyncTask>(cancelToken, future) - .then((d) { - if (cancelToken != null) { - httpClient.close(); - } - return d; - }); + return await _listenCancelForAsyncTask>(cancelToken, future); } - Future> _makeRequest( RequestOptions options, CancelToken cancelToken, - [HttpClient httpClient, OnUploadProgress onUploadProgress]) async { + [Completer cancelCompleter, OnUploadProgress onUploadProgress]) async { _checkCancelled(cancelToken); - HttpClientResponse response; + ResponseBody responseBody; try { - Future requestFuture; - // Handle timeout - if (options.connectTimeout > 0) { - requestFuture = httpClient - .openUrl(options.method, options.uri) - .timeout(new Duration(milliseconds: options.connectTimeout)); - } else { - requestFuture = httpClient.openUrl(options.method, options.uri); - } - HttpClientRequest request; - try { - request = await _listenCancelForAsyncTask(cancelToken, requestFuture); - } on TimeoutException { - throw new DioError( - request: options, - message: "Connecting timeout[${options.connectTimeout}ms]", - type: DioErrorType.CONNECT_TIMEOUT, - ); - } - request.followRedirects = options.followRedirects; - - try { - if (options.method != "GET") { - // Transform the request data, set headers inner. - await _listenCancelForAsyncTask( - cancelToken, _transformData(options, request, onUploadProgress)); - } else { - _setHeaders(options, request); - } - } catch (e) { - //If user cancel the request in transformer, close the connect by hand. - request.addError(e); - } - - response = await _listenCancelForAsyncTask(cancelToken, request.close()); + var stream = await _transformData(options); + responseBody = await httpClientAdapter.sendRequest( + options, + stream, + (future) => _listenCancelForAsyncTask(cancelToken, future), + cancelCompleter?.future, + ); Response ret = new Response( - headers: response.headers, + headers: responseBody.headers, request: options, - statusCode: response.statusCode); + statusCode: responseBody.statusCode); Future future; - bool statusOk = options.validateStatus(response.statusCode); + bool statusOk = options.validateStatus(responseBody.statusCode); if (statusOk || options.receiveDataWhenStatusError) { ret.data = await _listenCancelForAsyncTask( - cancelToken, transformer.transformResponse(options, response)); + cancelToken, transformer.transformResponse(options, responseBody)); } else { - response.drain(); + responseBody.stream.drain(); + if (cancelToken != null) cancelCompleter.complete(); } _checkCancelled(cancelToken); if (statusOk) { @@ -634,7 +636,7 @@ class Dio { } else { var err = new DioError( response: ret, - message: 'Http status error [${response.statusCode}]', + message: 'Http status error [${responseBody.statusCode}]', type: DioErrorType.RESPONSE, ); future = _onError(err); @@ -643,7 +645,7 @@ class Dio { } catch (e) { DioError err = _assureDioError(e); if (CancelToken.isCancel(err)) { - httpClient.close(force: true); + cancelCompleter?.complete(); throw err; } else { // Response onError @@ -679,15 +681,15 @@ class Dio { } } - _transformData(RequestOptions options, HttpClientRequest request, + Future>> _transformData(RequestOptions options, [OnUploadProgress onUploadProgress]) async { var data = options.data; List bytes; if (data != null && ["POST", "PUT", "PATCH"].contains(options.method)) { // Handle the FormData if (data is FormData) { - request.headers.set(HttpHeaders.contentTypeHeader, - 'multipart/form-data; boundary=${data.boundary.substring(2)}'); + options.headers[HttpHeaders.contentTypeHeader] = + 'multipart/form-data; boundary=${data.boundary.substring(2)}'; bytes = data.bytes(); } else { options.headers[HttpHeaders.contentTypeHeader] = @@ -702,11 +704,7 @@ class Dio { bytes = utf8.encode(_data); } } - - // Must set the content-length - request.contentLength = bytes.length; - _setHeaders(options, request); - + options.headers[HttpHeaders.contentLengthHeader] = bytes.length; // support data sending progress int length = bytes.length; int complete = 0; @@ -726,10 +724,9 @@ class Dio { onUploadProgress(complete, length); } })); - await request.addStream(byteStream); - } else { - _setHeaders(options, request); + return byteStream; } + return null; } // Transform current Future status("success" and "error") if necessary @@ -765,13 +762,14 @@ class Dio { RequestOptions _mergeOptions( Options opt, String url, data, Map queryParameters) { - var query= (new Map.from(options.queryParameters??{})) - ..addAll(queryParameters??{}); + var query = (new Map.from(options.queryParameters ?? {})) + ..addAll(queryParameters ?? {}); return RequestOptions( method: opt.method.toUpperCase(), headers: (new Map.from(options.headers))..addAll(opt.headers), baseUrl: options.baseUrl ?? "", path: url, + data: data, connectTimeout: opt.connectTimeout ?? options.connectTimeout ?? 0, receiveTimeout: opt.receiveTimeout ?? options.receiveTimeout ?? 0, responseType: @@ -831,8 +829,4 @@ class Dio { } return response; } - - void _setHeaders(RequestOptions options, HttpClientRequest request) { - options.headers.forEach((k, v) => request.headers.set(k, v)); - } } diff --git a/lib/src/options.dart b/lib/src/options.dart index 5e4800ac0..85632d119 100644 --- a/lib/src/options.dart +++ b/lib/src/options.dart @@ -16,7 +16,7 @@ enum ResponseType { typedef bool ValidateStatus(int status); -/// Dio instance request config +/// The common config for the Dio instance. /// `dio.options` is a instance of [BaseOptions] class BaseOptions extends _RequestConfig { BaseOptions({ @@ -82,6 +82,7 @@ class BaseOptions extends _RequestConfig { /// Request base url, it can contain sub path, like: "https://www.google.com/api/". String baseUrl; + /// Common query parameters Map*/ > queryParameters; } @@ -279,6 +280,6 @@ class _RequestConfig { /// see [HttpClientRequest.followRedirects] bool followRedirects; - /// Custom Cookies + /// Custom Cookies for every request List cookies; } diff --git a/lib/src/transformer.dart b/lib/src/transformer.dart index 6e74929d4..45bc64dc1 100644 --- a/lib/src/transformer.dart +++ b/lib/src/transformer.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'dio_error.dart'; import 'options.dart'; +import 'adapter.dart'; /// [Transformer] allows changes to the request/response data before /// it is sent/received to/from the server. @@ -26,7 +27,7 @@ abstract class Transformer { /// /// **Note**: As an agreement, you must return the [response] /// when the Options.responseType is [ResponseType.stream]. - Future transformResponse(RequestOptions options, HttpClientResponse response); + Future transformResponse(RequestOptions options, ResponseBody response); /// Deep encode the [Map] to percent-encoding. /// It is mostly used with the "application/x-www-form-urlencoded" content-type. @@ -79,12 +80,12 @@ class DefaultTransformer extends Transformer { /// As an agreement, you must return the [response] /// when the Options.responseType is [ResponseType.stream]. - Future transformResponse(RequestOptions options, HttpClientResponse response) async { + Future transformResponse(RequestOptions options, ResponseBody response) async { if (options.responseType == ResponseType.stream) { return response; } // Handle timeout - Stream> stream = response; + Stream> stream = response.stream; if (options.receiveTimeout > 0) { stream = stream.timeout( new Duration(milliseconds: options.receiveTimeout), diff --git a/pubspec.yaml b/pubspec.yaml index fd765d308..2371d8aba 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: dio description: A powerful Http client for Dart, which supports Interceptors, FormData, Request Cancellation, File Downloading, Timeout etc. -version: 1.1.0 +version: 2.0.0 homepage: https://github.com/flutterchina/dio author: wendux <824783146@qq.com> diff --git a/test/dio_test.dart b/test/dio_test.dart index 82ca2dca9..a1ad3b05d 100644 --- a/test/dio_test.dart +++ b/test/dio_test.dart @@ -17,8 +17,8 @@ class MyTransformer extends DefaultTransformer { /// info to [Options.extra], and you can retrieve it in [ResponseInterceptor] /// and [Response] with `response.request.extra["cookies"]`. @override - Future transformResponse(Options options, HttpClientResponse response) async { - options.extra["cookies"] = response.cookies; + Future transformResponse(RequestOptions options, ResponseBody response) async { + options.extra["xx"] = "extra"; return super.transformResponse(options, response); } } @@ -41,7 +41,7 @@ void main() { }); test('test', () async { Response response; - response = await dio.get("/test"); + response = await dio.get("/test", queryParameters: {"id": '12', "name": "wendu"}); expect(response.data["errCode"], 0); response = await dio.post("/test"); expect(response.data["errCode"], 0); @@ -146,15 +146,19 @@ void main() { group('transfomer', () { test("test", () async { var dio = new Dio(); + dio.options.baseUrl="http://www.dtworkroom.com/doris/1/2.0.0"; dio.transformer = new MyTransformer(); // Response response = await dio.get("https://www.baidu.com"); // assert(response.request.extra["cookies"]!=null); try { - var r = await dio.post("http://www.dtworkroom.com/doris/1/2.0.0/test", + var r = await dio.post("/test", data: ["1", "2"]); } catch (e) { expect(e.message, "Can't send List to sever directly"); } + dio.get("/test").then((r){ + expect(r.request.extra["xx"], "extra"); + }); var data = { "a": "你好", "b": [5, "6"],