diff --git a/.github/workflows/dio.yml b/.github/workflows/dio.yml index acdfacfa9..64fdd00d1 100644 --- a/.github/workflows/dio.yml +++ b/.github/workflows/dio.yml @@ -44,7 +44,8 @@ jobs: strategy: fail-fast: false matrix: - sdk: [ stable, beta, dev, 2.12.1 ] + sdk: [ stable, beta, 2.12.1 ] + platform: [ vm, chrome ] defaults: run: working-directory: dio @@ -54,12 +55,12 @@ jobs: with: sdk: ${{ matrix.sdk }} - run: pub get - - run: dart test --chain-stack-traces + - run: dart test --chain-stack-traces --platform=${{ matrix.platform }} - name: Upload test report uses: actions/upload-artifact@v2 - if: always() && matrix.sdk == 'stable' + if: always() with: - name: test-results + name: test-results-${{ matrix.sdk }}-${{ matrix.platform }} path: dio/build/reports/test-results.json publish-dry-run: diff --git a/.github/workflows/example-app.yml b/.github/workflows/example-app.yml index 333386305..ecffdc135 100644 --- a/.github/workflows/example-app.yml +++ b/.github/workflows/example-app.yml @@ -19,7 +19,7 @@ jobs: working-directory: example_flutter_app steps: - uses: actions/checkout@v2 - - uses: subosito/flutter-action@v2.2.1 + - uses: subosito/flutter-action@v2.3.0 with: cache: true channel: stable @@ -34,7 +34,7 @@ jobs: working-directory: example_flutter_app steps: - uses: actions/checkout@v2 - - uses: subosito/flutter-action@v2.2.1 + - uses: subosito/flutter-action@v2.3.0 with: cache: true channel: stable @@ -48,7 +48,7 @@ jobs: working-directory: example_flutter_app steps: - uses: actions/checkout@v2 - - uses: subosito/flutter-action@v2.2.1 + - uses: subosito/flutter-action@v2.3.0 with: cache: true channel: stable diff --git a/.github/workflows/plugin_cookie_manager.yml b/.github/workflows/plugin_cookie_manager.yml index d839e447e..f8c83284b 100644 --- a/.github/workflows/plugin_cookie_manager.yml +++ b/.github/workflows/plugin_cookie_manager.yml @@ -44,7 +44,7 @@ jobs: strategy: fail-fast: false matrix: - sdk: [ stable, beta, dev, 2.12.1 ] + sdk: [ stable, beta, 2.12.1 ] defaults: run: working-directory: plugins/cookie_manager @@ -57,7 +57,7 @@ jobs: - run: dart test --chain-stack-traces - name: Upload test report uses: actions/upload-artifact@v2 - if: always() && matrix.sdk == 'stable' + if: always() with: - name: test-results + name: test-results-${{ matrix.sdk }} path: plugins/cookie_manager/build/reports/test-results.json diff --git a/.github/workflows/plugin_http2_adapter.yml b/.github/workflows/plugin_http2_adapter.yml index f0c81e314..6433dd729 100644 --- a/.github/workflows/plugin_http2_adapter.yml +++ b/.github/workflows/plugin_http2_adapter.yml @@ -44,7 +44,7 @@ jobs: strategy: fail-fast: false matrix: - sdk: [ stable, beta, dev, 2.12.1 ] + sdk: [ stable, beta, 2.12.1 ] defaults: run: working-directory: plugins/http2_adapter @@ -57,7 +57,7 @@ jobs: - run: dart test --chain-stack-traces - name: Upload test report uses: actions/upload-artifact@v2 - if: always() && matrix.sdk == 'stable' + if: always() with: - name: test-results + name: test-results-${{ matrix.sdk }} path: plugins/http2_adapter/build/reports/test-results.json diff --git a/.github/workflows/test-report.yml b/.github/workflows/test-report.yml index 3401fbb75..41c8cd267 100644 --- a/.github/workflows/test-report.yml +++ b/.github/workflows/test-report.yml @@ -14,10 +14,24 @@ jobs: report: runs-on: ubuntu-latest steps: + - uses: actions/checkout@v2 + - name: Download and Extract Artifacts + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + run: | + mkdir -p artifacts && cd artifacts + + artifacts_url=${{ github.event.workflow_run.artifacts_url }} + + gh api "$artifacts_url" -q '.artifacts[] | [.name, .archive_download_url] | @tsv' | while read artifact + do + IFS=$'\t' read name url <<< "$artifact" + gh api $url > "$name.zip" + unzip -d "$name" "$name.zip" + done - name: Publish test reports uses: dorny/test-reporter@v1 with: name: ${{ github.event.workflow_run.workflow.name }} Test results - artifact: test-results - path: '**/*.json' + path: 'artifacts/**/test-results.json' reporter: dart-json diff --git a/README-ZH.md b/README-ZH.md index 0340ccdf1..ea92546b2 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -13,7 +13,7 @@ dio是一个强大的Dart Http请求库,支持Restful API、FormData、拦截 ```yaml dependencies: - dio: ^4.0.5-beta1 + dio: ^4.0.6 ``` > 如果你是dio 3.x 用户,想了解4.0的变更,请参考 [4.x更新列表](./migration_to_4.x.md)! @@ -39,11 +39,12 @@ void getHttp() async { | Plugins | Status | Description | | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | -| [dio_cookie_manager](https://github.com/flutterchina/dio/tree/master/plugins/cookie_manager) | [![Pub](https://img.shields.io/pub/v/dio_http2_adapter.svg?style=flat-square)](https://pub.dartlang.org/packages/dio_http2_adapter) | A cookie manager for Dio | -| [dio_http2_adapter](https://github.com/flutterchina/dio/tree/master/plugins/http2_adapter) | [![Pub](https://img.shields.io/pub/v/dio_cookie_manager.svg?style=flat-square)](https://pub.dartlang.org/packages/dio_cookie_manager) | A Dio HttpClientAdapter which support Http/2.0 | +| [dio_cookie_manager](https://github.com/flutterchina/dio/tree/master/plugins/cookie_manager) | [![Pub](https://img.shields.io/pub/v/dio_cookie_manager.svg?style=flat-square)](https://pub.dartlang.org/packages/dio_cookie_manager) | A cookie manager for Dio | +| [dio_http2_adapter](https://github.com/flutterchina/dio/tree/master/plugins/http2_adapter) | [![Pub](https://img.shields.io/pub/v/dio_http2_adapter.svg?style=flat-square)](https://pub.dartlang.org/packages/dio_http2_adapter) | A Dio HttpClientAdapter which support Http/2.0 | | [dio_smart_retry](https://github.com/rodion-m/dio_smart_retry) | [![Pub](https://img.shields.io/pub/v/dio_smart_retry.svg?style=flat-square)](https://pub.dev/packages/dio_smart_retry) | Flexible retry library for Dio | | [http_certificate_pinning](https://github.com/diefferson/http_certificate_pinning) | [![Pub](https://img.shields.io/pub/v/http_certificate_pinning.svg?style=flat-square)](https://pub.dev/packages/http_certificate_pinning) | Https Certificate pinning for Flutter | | [curl_logger_dio_interceptor](https://github.com/OwnWeb/curl_logger_dio_interceptor) | [![Pub](https://img.shields.io/pub/v/curl_logger_dio_interceptor.svg?style=flat-square)](https://pub.dev/packages/curl_logger_dio_interceptor) | A Flutter curl-command generator for Dio. | +| [dio_cache_interceptor](https://github.com/llfbandit/dio_cache_interceptor) | [![Pub](https://img.shields.io/pub/v/dio_cache_interceptor.svg?style=flat-square)](https://pub.dev/packages/dio_cache_interceptor) | Dio HTTP cache interceptor with multiple stores respecting HTTP directives (or not) | | [dio_http_cache](https://github.com/hurshi/dio-http-cache) | [![Pub](https://img.shields.io/pub/v/dio_http_cache.svg?style=flat-square)](https://pub.dev/packages/dio_http_cache) | A simple cache library for Dio like Rxcache in Android | | [pretty_dio_logger](https://github.com/Milad-Akarie/pretty_dio_logger) | [![Pub](https://img.shields.io/pub/v/pretty_dio_logger.svg?style=flat-square)](https://pub.dev/packages/pretty_dio_logger) | Pretty Dio logger is a Dio interceptor that logs network calls in a pretty, easy to read format. | diff --git a/README.md b/README.md index 99016c8a1..2c48d9bca 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ A powerful Http client for Dart, which supports Interceptors, Global configurati ```yaml dependencies: - dio: ^4.0.5-beta1 + dio: ^4.0.6 ``` > Already know Dio 3 and just want to learn about what's new in Dio 4? Check out the [Migration Guide](./migration_to_4.x.md)! @@ -38,11 +38,12 @@ void getHttp() async { | Plugins | Status | Description | | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | -| [dio_cookie_manager](https://github.com/flutterchina/dio/tree/master/plugins/cookie_manager) | [![Pub](https://img.shields.io/pub/v/dio_http2_adapter.svg?style=flat-square)](https://pub.dartlang.org/packages/dio_http2_adapter) | A cookie manager for Dio | -| [dio_http2_adapter](https://github.com/flutterchina/dio/tree/master/plugins/http2_adapter) | [![Pub](https://img.shields.io/pub/v/dio_cookie_manager.svg?style=flat-square)](https://pub.dartlang.org/packages/dio_cookie_manager) | A Dio HttpClientAdapter which support Http/2.0 | +| [dio_cookie_manager](https://github.com/flutterchina/dio/tree/master/plugins/cookie_manager) | [![Pub](https://img.shields.io/pub/v/dio_cookie_manager.svg?style=flat-square)](https://pub.dartlang.org/packages/dio_cookie_manager) | A cookie manager for Dio | +| [dio_http2_adapter](https://github.com/flutterchina/dio/tree/master/plugins/http2_adapter) | [![Pub](https://img.shields.io/pub/v/dio_http2_adapter.svg?style=flat-square)](https://pub.dartlang.org/packages/dio_http2_adapter) | A Dio HttpClientAdapter which support Http/2.0 | | [dio_smart_retry](https://github.com/rodion-m/dio_smart_retry) | [![Pub](https://img.shields.io/pub/v/dio_smart_retry.svg?style=flat-square)](https://pub.dev/packages/dio_smart_retry) | Flexible retry library for Dio | | [http_certificate_pinning](https://github.com/diefferson/http_certificate_pinning) | [![Pub](https://img.shields.io/pub/v/http_certificate_pinning.svg?style=flat-square)](https://pub.dev/packages/http_certificate_pinning) | Https Certificate pinning for Flutter | | [curl_logger_dio_interceptor](https://github.com/OwnWeb/curl_logger_dio_interceptor) | [![Pub](https://img.shields.io/pub/v/curl_logger_dio_interceptor.svg?style=flat-square)](https://pub.dev/packages/curl_logger_dio_interceptor) | A Flutter curl-command generator for Dio. | +| [dio_cache_interceptor](https://github.com/llfbandit/dio_cache_interceptor) | [![Pub](https://img.shields.io/pub/v/dio_cache_interceptor.svg?style=flat-square)](https://pub.dev/packages/dio_cache_interceptor) | Dio HTTP cache interceptor with multiple stores respecting HTTP directives (or not) | | [dio_http_cache](https://github.com/hurshi/dio-http-cache) | [![Pub](https://img.shields.io/pub/v/dio_http_cache.svg?style=flat-square)](https://pub.dev/packages/dio_http_cache) | A simple cache library for Dio like Rxcache in Android | | [pretty_dio_logger](https://github.com/Milad-Akarie/pretty_dio_logger) | [![Pub](https://img.shields.io/pub/v/pretty_dio_logger.svg?style=flat-square)](https://pub.dev/packages/pretty_dio_logger) | Pretty Dio logger is a Dio interceptor that logs network calls in a pretty, easy to read format. | @@ -71,9 +72,9 @@ Welcome to submit Dio's third-party plugins and related libraries [here](https:/ - [Sending FormData](#sending-formdata) -- [Transformer](#Transformer) +- [Transformer](#transformer) -- [Set proxy and HttpClient config](#set-proxy-and-httpclient-config) +- [Using proxy](#using-proxy) - [Https certificate verification](#https-certificate-verification) diff --git a/dio/CHANGELOG.md b/dio/CHANGELOG.md index 3b49c2845..a05064e62 100644 --- a/dio/CHANGELOG.md +++ b/dio/CHANGELOG.md @@ -1,17 +1,23 @@ -# Unreleased -- require Dart `2.12.1` which fixes exception handling for secure socket connections (https://github.com/dart-lang/sdk/issues/45214) -- `ResponseBody.statusCode` is now non-nullable -- Only delete file if it exists when downloading. - # Unreleased 5.0 -- add option to instantiate a `HttpClientAdapter`, which is platform independent -# 4.0.5-beta1 +- Add option to instantiate a `HttpClientAdapter`, which is platform independent + +# 4.0.6 + +- fix #1452 + +# 4.0.5 + +- require Dart `2.12.1` which fixes exception handling for secure socket connections (https://github.com/dart-lang/sdk/issues/45214) +- Only delete file if it exists when downloading. +- Fix `BrowserHttpClientAdapter` canceled hangs +- Correct JSON MIME Type detection - [Web] support send/receive progress in web platform - refactor timeout logic - use 'arraybuffer' instead of 'blob' for xhr requests in web platform # 4.0.4 + - Fix fetching null data in a response # 4.0.3 @@ -247,4 +253,4 @@ First Stable version for 2.x ## 0.0.1 -- Initial version, created by Stagehand +- Initial version, created by Stagehand \ No newline at end of file diff --git a/dio/README-ZH.md b/dio/README-ZH.md index 87a9d4a47..8cb550db9 100644 --- a/dio/README-ZH.md +++ b/dio/README-ZH.md @@ -13,7 +13,7 @@ dio是一个强大的Dart Http请求库,支持Restful API、FormData、拦截 ```yaml dependencies: - dio: ^4.0.5-beta1 + dio: ^4.0.6 ``` > 如果你是dio 3.x 用户,想了解4.0的变更,请参考 [4.x更新列表](./migration_to_4.x.md)! @@ -44,6 +44,7 @@ void getHttp() async { | [dio_smart_retry](https://github.com/rodion-m/dio_smart_retry) | [![Pub](https://img.shields.io/pub/v/dio_smart_retry.svg?style=flat-square)](https://pub.dev/packages/dio_smart_retry) | Flexible retry library for Dio | | [http_certificate_pinning](https://github.com/diefferson/http_certificate_pinning) | [![Pub](https://img.shields.io/pub/v/http_certificate_pinning.svg?style=flat-square)](https://pub.dev/packages/http_certificate_pinning) | Https Certificate pinning for Flutter | | [curl_logger_dio_interceptor](https://github.com/OwnWeb/curl_logger_dio_interceptor) | [![Pub](https://img.shields.io/pub/v/curl_logger_dio_interceptor.svg?style=flat-square)](https://pub.dev/packages/curl_logger_dio_interceptor) | A Flutter curl-command generator for Dio. | +| [dio_cache_interceptor](https://github.com/llfbandit/dio_cache_interceptor) | [![Pub](https://img.shields.io/pub/v/dio_cache_interceptor.svg?style=flat-square)](https://pub.dev/packages/dio_cache_interceptor) | Dio HTTP cache interceptor with multiple stores respecting HTTP directives (or not) | | [dio_http_cache](https://github.com/hurshi/dio-http-cache) | [![Pub](https://img.shields.io/pub/v/dio_http_cache.svg?style=flat-square)](https://pub.dev/packages/dio_http_cache) | A simple cache library for Dio like Rxcache in Android | | [pretty_dio_logger](https://github.com/Milad-Akarie/pretty_dio_logger) | [![Pub](https://img.shields.io/pub/v/pretty_dio_logger.svg?style=flat-square)](https://pub.dev/packages/pretty_dio_logger) | Pretty Dio logger is a Dio interceptor that logs network calls in a pretty, easy to read format. | diff --git a/dio/README.md b/dio/README.md index effbeb875..181688847 100644 --- a/dio/README.md +++ b/dio/README.md @@ -12,7 +12,7 @@ A powerful Http client for Dart, which supports Interceptors, Global configurati ```yaml dependencies: - dio: ^4.0.5-beta1 + dio: ^4.0.6 ``` > Already know Dio 3 and just want to learn about what's new in Dio 4? Check out the [Migration Guide](./migration_to_4.x.md)! @@ -43,6 +43,7 @@ void getHttp() async { | [dio_smart_retry](https://github.com/rodion-m/dio_smart_retry) | [![Pub](https://img.shields.io/pub/v/dio_smart_retry.svg?style=flat-square)](https://pub.dev/packages/dio_smart_retry) | Flexible retry library for Dio | | [http_certificate_pinning](https://github.com/diefferson/http_certificate_pinning) | [![Pub](https://img.shields.io/pub/v/http_certificate_pinning.svg?style=flat-square)](https://pub.dev/packages/http_certificate_pinning) | Https Certificate pinning for Flutter | | [curl_logger_dio_interceptor](https://github.com/OwnWeb/curl_logger_dio_interceptor) | [![Pub](https://img.shields.io/pub/v/curl_logger_dio_interceptor.svg?style=flat-square)](https://pub.dev/packages/curl_logger_dio_interceptor) | A Flutter curl-command generator for Dio. | +| [dio_cache_interceptor](https://github.com/llfbandit/dio_cache_interceptor) | [![Pub](https://img.shields.io/pub/v/dio_cache_interceptor.svg?style=flat-square)](https://pub.dev/packages/dio_cache_interceptor) | Dio HTTP cache interceptor with multiple stores respecting HTTP directives (or not) | | [dio_http_cache](https://github.com/hurshi/dio-http-cache) | [![Pub](https://img.shields.io/pub/v/dio_http_cache.svg?style=flat-square)](https://pub.dev/packages/dio_http_cache) | A simple cache library for Dio like Rxcache in Android | | [pretty_dio_logger](https://github.com/Milad-Akarie/pretty_dio_logger) | [![Pub](https://img.shields.io/pub/v/pretty_dio_logger.svg?style=flat-square)](https://pub.dev/packages/pretty_dio_logger) | Pretty Dio logger is a Dio interceptor that logs network calls in a pretty, easy to read format. | diff --git a/dio/lib/src/adapter.dart b/dio/lib/src/adapter.dart index f0b1a65f1..1f6a1fbcb 100644 --- a/dio/lib/src/adapter.dart +++ b/dio/lib/src/adapter.dart @@ -68,10 +68,10 @@ class ResponseBody { Stream stream; /// the response headers - Map> headers; + late Map> headers; /// Http status code - int statusCode; + int? statusCode; /// Returns the reason phrase associated with the status code. /// The reason phrase must be set before the body is written diff --git a/dio/lib/src/adapters/browser_adapter.dart b/dio/lib/src/adapters/browser_adapter.dart index 6a9027844..8d441fa62 100644 --- a/dio/lib/src/adapters/browser_adapter.dart +++ b/dio/lib/src/adapters/browser_adapter.dart @@ -60,7 +60,7 @@ class BrowserHttpClientAdapter implements HttpClientAdapter { completer.complete( ResponseBody.fromBytes( body, - xhr.status!, + xhr.status, headers: xhr.responseHeaders.map((k, v) => MapEntry(k, v.split(','))), statusMessage: xhr.statusText, isRedirect: xhr.status == 302 || xhr.status == 301, @@ -74,17 +74,17 @@ class BrowserHttpClientAdapter implements HttpClientAdapter { if (connectionTimeout != null) { Future.delayed(connectionTimeout).then( (value) { - if (!haveSent) { - completer.completeError( - DioError( - requestOptions: options, - error: 'Connecting timed out [${options.connectTimeout}ms]', - type: DioErrorType.connectTimeout, - ), - StackTrace.current, - ); - xhr.abort(); + if (haveSent) { + return; } + completer.completeError( + DioError.connectionTimeout( + requestOptions: options, + timeout: options.connectTimeout!, + ), + StackTrace.current, + ); + xhr.abort(); }, ); } @@ -102,10 +102,9 @@ class BrowserHttpClientAdapter implements HttpClientAdapter { if (duration > sendTimeout) { uploadStopwatch.stop(); completer.completeError( - DioError( + DioError.sendTimeout( + timeout: options.sendTimeout!, requestOptions: options, - error: 'Sending timed out [${options.sendTimeout}ms]', - type: DioErrorType.sendTimeout, ), StackTrace.current, ); @@ -131,10 +130,9 @@ class BrowserHttpClientAdapter implements HttpClientAdapter { if (duration > reveiveTimeout) { downloadStopwatch.stop(); completer.completeError( - DioError( + DioError.receiveTimeout( + timeout: options.receiveTimeout!, requestOptions: options, - error: 'Receiving timed out [${options.receiveTimeout}ms]', - type: DioErrorType.receiveTimeout, ), StackTrace.current, ); @@ -151,23 +149,31 @@ class BrowserHttpClientAdapter implements HttpClientAdapter { xhr.onError.first.then((_) { // Unfortunately, the underlying XMLHttpRequest API doesn't expose any // specific information about the error itself. + // See also: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequestEventTarget/onerror completer.completeError( - DioError( - type: DioErrorType.response, - error: 'XMLHttpRequest error.', + DioError.connectionError( requestOptions: options, + reason: 'The XMLHttpRequest onError callback was called. ' + 'This typically indicates an error on the network layer.', ), StackTrace.current, ); }); - cancelFuture?.then((_) { + cancelFuture?.then((err) { if (xhr.readyState < 4 && xhr.readyState > 0) { try { xhr.abort(); } catch (e) { // ignore } + + // xhr.onError will not triggered when xhr.abort() called. + // so need to manual throw the cancel error to avoid Future hang ups. + // or added xhr.onAbort like axios did https://github.com/axios/axios/blob/master/lib/adapters/xhr.js#L102-L111 + if (!completer.isCompleted) { + completer.completeError(err); + } } }); diff --git a/dio/lib/src/adapters/io_adapter.dart b/dio/lib/src/adapters/io_adapter.dart index e3a903403..1f30435b9 100644 --- a/dio/lib/src/adapters/io_adapter.dart +++ b/dio/lib/src/adapters/io_adapter.dart @@ -35,19 +35,19 @@ class DefaultHttpClientAdapter implements HttpClientAdapter { var _httpClient = _configHttpClient(cancelFuture, options.connectTimeout); var reqFuture = _httpClient.openUrl(options.method, options.uri); - void _throwConnectingTimeout() { - throw DioError( - requestOptions: options, - error: 'Connecting timed out [${options.connectTimeout}ms]', - type: DioErrorType.connectTimeout, - ); - } - late HttpClientRequest request; try { final connectionTimeout = options.connectTimeout; if (connectionTimeout != null) { - request = await reqFuture.timeout(connectionTimeout); + request = await reqFuture.timeout( + connectionTimeout, + onTimeout: () { + throw DioError.connectionTimeout( + requestOptions: options, + timeout: options.connectTimeout!, + ); + }, + ); } else { request = await reqFuture; } @@ -56,13 +56,18 @@ class DefaultHttpClientAdapter implements HttpClientAdapter { options.headers.forEach((k, v) { if (v != null) request.headers.set(k, '$v'); }); - } on SocketException catch (e) { - if (e.message.contains('timed out')) { - _throwConnectingTimeout(); + } on SocketException catch (e, stackTrace) { + if (!e.message.contains('timed out')) { + rethrow; } - rethrow; - } on TimeoutException { - _throwConnectingTimeout(); + throw DioError.connectionTimeout( + requestOptions: options, + timeout: options.connectTimeout ?? + _httpClient.connectionTimeout ?? + Duration.zero, + error: e, + stackTrace: stackTrace, + ); } request.followRedirects = options.followRedirects; @@ -71,38 +76,41 @@ class DefaultHttpClientAdapter implements HttpClientAdapter { if (requestStream != null) { // Transform the request data var future = request.addStream(requestStream); + final sendTimeout = options.sendTimeout; if (sendTimeout != null) { - future = future.timeout(sendTimeout); - } - try { - await future; - } on TimeoutException { - throw DioError( - requestOptions: options, - error: 'Sending timeout[${options.sendTimeout}ms]', - type: DioErrorType.sendTimeout, + future = future.timeout( + sendTimeout, + onTimeout: () { + request.abort(); + throw DioError.sendTimeout( + timeout: sendTimeout, + requestOptions: options, + ); + }, ); } + + await future; } final stopwatch = Stopwatch()..start(); var future = request.close(); final receiveTimeout = options.receiveTimeout; if (receiveTimeout != null) { - future = future.timeout(receiveTimeout); - } - late HttpClientResponse responseStream; - try { - responseStream = await future; - } on TimeoutException { - throw DioError( - requestOptions: options, - error: 'Receiving data timeout[${options.receiveTimeout}]', - type: DioErrorType.receiveTimeout, + future = future.timeout( + receiveTimeout, + onTimeout: () { + throw DioError.receiveTimeout( + timeout: receiveTimeout, + requestOptions: options, + ); + }, ); } + final responseStream = await future; + var stream = responseStream.transform(StreamTransformer.fromHandlers( handleData: (data, sink) { @@ -111,14 +119,12 @@ class DefaultHttpClientAdapter implements HttpClientAdapter { final receiveTimeout = options.receiveTimeout; if (receiveTimeout != null && duration > receiveTimeout) { sink.addError( - DioError( + DioError.receiveTimeout( + timeout: receiveTimeout, requestOptions: options, - error: 'Receiving data timeout[${options.receiveTimeout}]', - type: DioErrorType.receiveTimeout, ), ); - //todo: to verify - responseStream.detachSocket().then((socket) => socket.close()); + responseStream.detachSocket().then((socket) => socket.destroy()); } else { sink.add(Uint8List.fromList(data)); } diff --git a/dio/lib/src/cancel_token.dart b/dio/lib/src/cancel_token.dart index 59baed088..7b87f4d5d 100644 --- a/dio/lib/src/cancel_token.dart +++ b/dio/lib/src/cancel_token.dart @@ -13,7 +13,7 @@ class CancelToken { /// Whether is throw by [cancel] static bool isCancel(DioError e) { - return e.type == DioErrorType.cancel; + return e.type == DioErrorType.requestCancelled; } /// If request have been canceled, save the cancel Error. @@ -33,13 +33,12 @@ class CancelToken { Future get whenCancel => _completer.future; /// Cancel the request - void cancel([dynamic reason]) { - _cancelError = DioError( - type: DioErrorType.cancel, - error: reason, + void cancel([Object? reason]) { + _cancelError = DioError.requestCancelled( requestOptions: requestOptions ?? RequestOptions(path: ''), + reason: reason, + stackTrace: StackTrace.current, ); - _cancelError!.stackTrace = StackTrace.current; if (!_completer.isCompleted) { _completer.complete(_cancelError); diff --git a/dio/lib/src/dio.dart b/dio/lib/src/dio.dart index ce7a7e718..46442ca39 100644 --- a/dio/lib/src/dio.dart +++ b/dio/lib/src/dio.dart @@ -180,16 +180,23 @@ abstract class Dio { /// /// Dio will enqueue the incoming request tasks instead /// send them directly when [interceptor.requestOptions] is locked. - + @Deprecated( + 'Will delete in v5.0. Use `QueuedInterceptor` instead, more detail see' + ' https://github.com/flutterchina/dio/issues/1308') void lock(); /// Unlock the current Dio instance. /// /// Dio instance dequeue the request task。 + @Deprecated( + 'Will delete in v5.0. Use `QueuedInterceptor` instead, more detail see' + ' https://github.com/flutterchina/dio/issues/1308') void unlock(); ///Clear the current Dio instance waiting queue. - + @Deprecated( + 'Will delete in v5.0. Use `QueuedInterceptor` instead, more detail see' + ' https://github.com/flutterchina/dio/issues/1308') void clear(); /// Download the file and save it in local. The default http method is "GET", diff --git a/dio/lib/src/dio_error.dart b/dio/lib/src/dio_error.dart index b76e1f88f..d8cfeec0e 100644 --- a/dio/lib/src/dio_error.dart +++ b/dio/lib/src/dio_error.dart @@ -2,8 +2,8 @@ import 'options.dart'; import 'response.dart'; enum DioErrorType { - /// It occurs when url is opened timeout. - connectTimeout, + /// Caused by a connection timeout. + connectionTimeout, /// It occurs when url is sent timeout. sendTimeout, @@ -11,26 +11,100 @@ enum DioErrorType { ///It occurs when receiving timeout. receiveTimeout, - /// When the server response, but with a incorrect status, such as 404, 503... - response, + /// The [DioError] was caused by an incorrect status code as configured by + /// [ValidateStatus]. + badResponse, /// When the request is cancelled, dio will throw a error with this type. - cancel, + requestCancelled, + + /// Caused for example by a `xhr.onError` or SocketExceptions. + connectionError, /// Default error type, Some other Error. In this case, you can /// use the DioError.error if it is not null. - other, + unknown, +} + +extension _DioErrorTypeExtension on DioErrorType { + String toPrettyDescription() { + switch (this) { + case DioErrorType.connectionTimeout: + return 'connection timeout'; + case DioErrorType.sendTimeout: + return 'send timeout'; + case DioErrorType.receiveTimeout: + return 'receive timeout'; + case DioErrorType.badResponse: + return 'bad response'; + case DioErrorType.requestCancelled: + return 'request cancelled'; + case DioErrorType.connectionError: + return 'connection error'; + case DioErrorType.unknown: + return 'unknown'; + } + } } -/// DioError describes the error info when request failed. +/// DioError describes the exception info when a request failed. class DioError implements Exception { + /// Prefer using one of the other constructors. + /// They're most likely better fitting. DioError({ required this.requestOptions, this.response, - this.type = DioErrorType.other, + this.type = DioErrorType.unknown, this.error, + this.stackTrace, + this.message, }); + DioError.badResponse({ + required int statusCode, + required this.requestOptions, + required this.response, + }) : type = DioErrorType.badResponse, + message = 'The request returned an ' + 'invalid status code of $statusCode'; + + DioError.connectionTimeout({ + required Duration timeout, + required this.requestOptions, + this.error, + this.stackTrace, + }) : type = DioErrorType.connectionTimeout, + message = 'The request connection took ' + 'longer than $timeout. It was aborted.'; + + DioError.sendTimeout({ + required Duration timeout, + required this.requestOptions, + }) : type = DioErrorType.sendTimeout, + message = 'The request took ' + 'longer than $timeout to send data. It was aborted.'; + + DioError.receiveTimeout({ + required Duration timeout, + required this.requestOptions, + }) : type = DioErrorType.receiveTimeout, + message = 'The request took ' + 'longer than $timeout to receive data. It was aborted.'; + + DioError.requestCancelled({ + required this.requestOptions, + required Object? reason, + this.stackTrace, + }) : type = DioErrorType.requestCancelled, + message = 'The request was cancelled.', + error = reason; + + DioError.connectionError({ + required this.requestOptions, + required String reason, + }) : type = DioErrorType.connectionError, + message = 'The connection errored: $reason'; + /// Request info. RequestOptions requestOptions; @@ -42,20 +116,26 @@ class DioError implements Exception { /// The original error/exception object; It's usually not null when `type` /// is DioErrorType.other - dynamic error; + Object? error; + /// The stacktrace of the original error/exception object; + /// It's usually not null when `type` is DioErrorType.other StackTrace? stackTrace; - String get message => (error?.toString() ?? ''); + String? message; @override String toString() { - var msg = 'DioError [$type]: $message'; - if (error is Error) { - msg += '\n${(error as Error).stackTrace}'; + var msg = 'DioError [${type.toPrettyDescription()}]: $message'; + final error = this.error; + if (error != null) { + msg += '\nError: $error'; + } + if (error is Error && error.stackTrace != stackTrace) { + msg += '\nInner error stacktrace:\n${error.stackTrace}'; } if (stackTrace != null) { - msg += '\nSource stack:\n$stackTrace'; + msg += '\nInner stacktrace:\n$stackTrace'; } return msg; } diff --git a/dio/lib/src/dio_mixin.dart b/dio/lib/src/dio_mixin.dart index e9ae16b9f..057a22a73 100644 --- a/dio/lib/src/dio_mixin.dart +++ b/dio/lib/src/dio_mixin.dart @@ -280,6 +280,9 @@ abstract class DioMixin implements Dio { /// /// Dio will enqueue the incoming request tasks instead /// send them directly when [interceptor.requestOptions] is locked. + @Deprecated( + 'Will delete in v5.0. Use `QueuedInterceptor` instead, more detail see' + ' https://github.com/flutterchina/dio/issues/1308') @override void lock() { interceptors.requestLock.lock(); @@ -288,12 +291,18 @@ abstract class DioMixin implements Dio { /// Unlock the current Dio instance. /// /// Dio instance dequeue the request task。 + @Deprecated( + 'Will delete in v5.0. Use `QueuedInterceptor` instead, more detail see' + ' https://github.com/flutterchina/dio/issues/1308') @override void unlock() { interceptors.requestLock.unlock(); } ///Clear the current Dio instance waiting queue. + @Deprecated( + 'Will delete in v5.0. Use `QueuedInterceptor` instead, more detail see' + ' https://github.com/flutterchina/dio/issues/1308') @override void clear() { interceptors.requestLock.clear(); @@ -464,9 +473,9 @@ abstract class DioMixin implements Dio { requestOptions.cancelToken = cancelToken; if (_closed) { - throw DioError( + throw DioError.connectionError( + reason: "Dio can't establish a new connection after it was closed.", requestOptions: requestOptions, - error: "Dio can't establish new connection after closed.", ); } @@ -475,11 +484,7 @@ abstract class DioMixin implements Dio { @override Future> fetch(RequestOptions requestOptions) async { - final stackTrace = StackTrace.current; - - if (requestOptions.cancelToken != null) { - requestOptions.cancelToken!.requestOptions = requestOptions; - } + requestOptions.cancelToken?.requestOptions = requestOptions; if (T != dynamic && !(requestOptions.responseType == ResponseType.bytes || @@ -550,6 +555,7 @@ abstract class DioMixin implements Dio { assureDioError( err, requestOptions, + stackTrace, ), ); } @@ -621,7 +627,7 @@ abstract class DioMixin implements Dio { data is InterceptorState ? data.data : data, requestOptions, ); - }).catchError((err, _) { + }).catchError((err, maybeStackTrace) { var isState = err is InterceptorState; if (isState) { @@ -630,6 +636,11 @@ abstract class DioMixin implements Dio { } } + StackTrace? stackTrace; + if (maybeStackTrace is StackTrace?) { + stackTrace = maybeStackTrace; + } + throw assureDioError( isState ? err.data : err, requestOptions, @@ -682,15 +693,14 @@ abstract class DioMixin implements Dio { if (statusOk) { return checkIfNeedEnqueue(interceptors.responseLock, () => ret); } else { - throw DioError( + throw DioError.badResponse( + statusCode: responseBody.statusCode!, requestOptions: reqOpt, response: ret, - error: 'Http status error [${responseBody.statusCode}]', - type: DioErrorType.response, ); } - } catch (e) { - throw assureDioError(e, reqOpt); + } catch (e, stackTrace) { + throw assureDioError(e, reqOpt, stackTrace); } } @@ -750,8 +760,9 @@ abstract class DioMixin implements Dio { // If the request has been cancelled, stop request and throw error. static void checkCancelled(CancelToken? cancelToken) { - if (cancelToken != null && cancelToken.cancelError != null) { - throw cancelToken.cancelError!; + final error = cancelToken?.cancelError; + if (error != null) { + throw error; } } @@ -782,23 +793,24 @@ abstract class DioMixin implements Dio { static DioError assureDioError( err, - RequestOptions requestOptions, [ + RequestOptions requestOptions, StackTrace? sourceStackTrace, - ]) { - DioError dioError; + ) { if (err is DioError) { - dioError = err; - } else { - dioError = DioError(requestOptions: requestOptions, error: err); + // nothing to be done + return err; } - - dioError.stackTrace = sourceStackTrace ?? dioError.stackTrace; - - return dioError; + return DioError( + requestOptions: requestOptions, + error: err, + stackTrace: sourceStackTrace, + ); } - static Response assureResponse(response, - [RequestOptions? requestOptions]) { + static Response assureResponse( + response, [ + RequestOptions? requestOptions, + ]) { if (response is! Response) { return Response( data: response as T, diff --git a/dio/lib/src/entry/dio_for_native.dart b/dio/lib/src/entry/dio_for_native.dart index ea267028a..87000d346 100644 --- a/dio/lib/src/entry/dio_for_native.dart +++ b/dio/lib/src/entry/dio_for_native.dart @@ -90,7 +90,7 @@ class DioForNative with DioMixin implements Dio { cancelToken: cancelToken ?? CancelToken(), ); } on DioError catch (e) { - if (e.type == DioErrorType.response) { + if (e.type == DioErrorType.badResponse) { if (e.response!.requestOptions.receiveDataWhenStatusError == true) { var res = await transformer.transformResponse( e.response!.requestOptions..responseType = ResponseType.json, @@ -183,6 +183,7 @@ class DioForNative with DioMixin implements Dio { completer.completeError(DioMixin.assureDioError( err, response.requestOptions, + stackTrace, )); } }); @@ -193,20 +194,22 @@ class DioForNative with DioMixin implements Dio { closed = true; await raf.close(); completer.complete(response); - } catch (e) { + } catch (e, stackTrace) { completer.completeError(DioMixin.assureDioError( e, response.requestOptions, + stackTrace, )); } }, - onError: (e) async { + onError: (e, stackTrace) async { try { await _closeAndDelete(); } finally { completer.completeError(DioMixin.assureDioError( e, response.requestOptions, + stackTrace as StackTrace?, )); } }, @@ -224,10 +227,9 @@ class DioForNative with DioMixin implements Dio { await subscription.cancel(); await _closeAndDelete(); if (err is TimeoutException) { - throw DioError( + throw DioError.receiveTimeout( + timeout: timeout, requestOptions: response.requestOptions, - error: 'Receiving data timeout[$timeout]', - type: DioErrorType.receiveTimeout, ); } else { throw err; diff --git a/dio/lib/src/options.dart b/dio/lib/src/options.dart index 8c5e33f6e..0a5657e11 100644 --- a/dio/lib/src/options.dart +++ b/dio/lib/src/options.dart @@ -194,7 +194,7 @@ mixin OptionsMixin { late Map queryParameters; /// Timeout in milliseconds for opening url. - /// [Dio] will throw the [DioError] with [DioErrorType.connectTimeout] type + /// [Dio] will throw the [DioError] with [DioErrorType.connectionTimeout] type /// when time out. Duration? get connectTimeout => _connectTimeout; diff --git a/dio/lib/src/transformer.dart b/dio/lib/src/transformer.dart index d24fc4cb8..55ebf2ebd 100644 --- a/dio/lib/src/transformer.dart +++ b/dio/lib/src/transformer.dart @@ -46,6 +46,15 @@ abstract class Transformer { listFormat: listFormat, ); } + + /// Following: https://mimesniff.spec.whatwg.org/#json-mime-type + static bool isJsonMimeType(String? contentType) { + if (contentType == null) return false; + final mediaType = MediaType.parse(contentType); + return mediaType.mimeType == 'application/json' || + mediaType.mimeType == 'text/json' || + mediaType.subtype.endsWith('+json'); + } } /// The default [Transformer] for [Dio]. If you want to custom the transformation of @@ -63,7 +72,7 @@ class DefaultTransformer extends Transformer { Future transformRequest(RequestOptions options) async { var data = options.data ?? ''; if (data is! String) { - if (_isJsonMime(options.contentType)) { + if (Transformer.isJsonMimeType(options.contentType)) { return json.encode(options.data); } else if (data is Map) { options.contentType = @@ -155,7 +164,8 @@ class DefaultTransformer extends Transformer { } if (responseBody.isNotEmpty && options.responseType == ResponseType.json && - _isJsonMime(response.headers[Headers.contentTypeHeader]?.first)) { + Transformer.isJsonMimeType( + response.headers[Headers.contentTypeHeader]?.first)) { final callback = jsonDecodeCallback; if (callback != null) { return callback(responseBody); @@ -165,10 +175,4 @@ class DefaultTransformer extends Transformer { } return responseBody; } - - bool _isJsonMime(String? contentType) { - if (contentType == null) return false; - return MediaType.parse(contentType).mimeType == - Headers.jsonMimeType.mimeType; - } } diff --git a/dio/pubspec.yaml b/dio/pubspec.yaml index 50770ed5e..86847c13f 100644 --- a/dio/pubspec.yaml +++ b/dio/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: 4.0.5-beta1 +version: 4.0.6 homepage: https://github.com/flutterchina/dio environment: diff --git a/dio/test/basic_test.dart b/dio/test/basic_test.dart index f59759b7c..e795f9cfa 100644 --- a/dio/test/basic_test.dart +++ b/dio/test/basic_test.dart @@ -1,7 +1,6 @@ // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -@TestOn('vm') import 'dart:async'; import 'dart:io'; @@ -46,7 +45,7 @@ void main() { Dio().get('http://http.invalid'), throwsA((e) => e is DioError && e.error is SocketException), ); - }); + }, testOn: "vm"); test('#cancellation', () async { var dio = Dio(); @@ -70,7 +69,7 @@ void main() { dio.get('401'), throwsA((e) => e is DioError && - e.type == DioErrorType.response && + e.type == DioErrorType.badResponse && e.response!.statusCode == 401), ); diff --git a/dio/test/download_test.dart b/dio/test/download_test.dart index 0b6e8cd59..ba85c05b0 100644 --- a/dio/test/download_test.dart +++ b/dio/test/download_test.dart @@ -89,7 +89,7 @@ void main() { cancelToken: cancelToken, ) .catchError((e) => throw (e as DioError).type), - throwsA(DioErrorType.cancel), + throwsA(DioErrorType.requestCancelled), ); //print(r); }); diff --git a/dio/test/exception_test.dart b/dio/test/exception_test.dart index d5aeb034b..cbcb05190 100644 --- a/dio/test/exception_test.dart +++ b/dio/test/exception_test.dart @@ -1,5 +1,7 @@ +import 'dart:io'; import 'package:dio/dio.dart'; import 'package:test/test.dart'; +import 'package:dio/adapter.dart'; void main() { test('catch DioError', () async { @@ -29,4 +31,32 @@ void main() { expect(error, isNotNull); expect(error is Exception, isTrue); }); + + test('catch sslerror: hostname mismatch', () async { + dynamic error; + + try { + await Dio().get('https://wrong.host.badssl.com/'); + fail('did not throw'); + } on DioError catch (e) { + error = e; + } + expect(error, isNotNull); + expect(error is Exception, isTrue); + }); + + test('allow badssl', () async { + var dio = Dio(); + (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = + (HttpClient client) { + client.badCertificateCallback = + (X509Certificate cert, String host, int port) => true; + }; + var response = await dio.get('https://wrong.host.badssl.com/'); + expect(response.statusCode, 200); + response = await dio.get('https://expired.badssl.com/'); + expect(response.statusCode, 200); + response = await dio.get('https://self-signed.badssl.com/'); + expect(response.statusCode, 200); + }, testOn: "!browser"); } diff --git a/dio/test/interceptor_test.dart b/dio/test/interceptor_test.dart index ec7a48359..bd8721b5e 100644 --- a/dio/test/interceptor_test.dart +++ b/dio/test/interceptor_test.dart @@ -122,7 +122,9 @@ void main() { if (err.requestOptions.path == '/reject-next/reject') { handler.reject(err); } else { - err.error++; + var count = (err.error as int); + count++; + err.error = count; handler.next(err); } } @@ -143,10 +145,14 @@ void main() { }, onError: (err, handler) { if (err.requestOptions.path == '/resolve-next/reject-next') { - err.error++; + var count = (err.error as int); + count++; + err.error = count; handler.next(err); } else { - err.error++; + var count = (err.error as int); + count++; + err.error = count; handler.next(err); } }, @@ -281,12 +287,12 @@ void main() { expect(response.data['errCode'], 0); expect( - dio.get('/fakepath3').catchError((e) => throw (e as DioError).message), - throwsA('test error'), + dio.get('/fakepath3').catchError((e) => throw (e as DioError)), + throwsA(isA()), ); expect( - dio.get('/fakepath4').catchError((e) => throw (e as DioError).message), - throwsA('test error'), + dio.get('/fakepath4').catchError((e) => throw (e as DioError)), + throwsA(isA()), ); response = await dio.get('/test'); @@ -354,10 +360,8 @@ void main() { response = await dio.get(urlNotFound + '2'); expect(response.data, 'fake data'); expect( - dio - .get(urlNotFound + '3') - .catchError((e) => throw (e as DioError).message), - throwsA('custom error info [404]'), + dio.get(urlNotFound + '3').catchError((e) => throw (e as DioError)), + throwsA(isA()), ); }); test('multi response interceptor', () async { diff --git a/dio/test/mimetype_test.dart b/dio/test/mimetype_test.dart new file mode 100644 index 000000000..5b2fe5e3e --- /dev/null +++ b/dio/test/mimetype_test.dart @@ -0,0 +1,17 @@ +import 'package:dio/dio.dart'; +import 'package:test/test.dart'; + +void main() { + test('JSON MimeType "application/json" ', () { + expect(Transformer.isJsonMimeType("application/json"), isTrue); + }); + + test('JSON MimeType "text/json" ', () { + expect(Transformer.isJsonMimeType("text/json"), isTrue); + }); + + test('JSON MimeType "application/vnd.example.com+json" ', () { + expect( + Transformer.isJsonMimeType("application/vnd.example.com+json"), isTrue); + }); +} diff --git a/dio/test/readtimeout_test.dart b/dio/test/readtimeout_test.dart index 26876329b..ae562f9a4 100644 --- a/dio/test/readtimeout_test.dart +++ b/dio/test/readtimeout_test.dart @@ -1,3 +1,4 @@ +@TestOn("vm") import 'dart:async'; import 'dart:io'; @@ -72,6 +73,7 @@ void main() { } expect(error, isNotNull); + //print(error); expect(error.type == DioErrorType.receiveTimeout, isTrue); }); diff --git a/dio/test/upload_stream_test.dart b/dio/test/upload_stream_test.dart index 142b32252..5facdf963 100644 --- a/dio/test/upload_stream_test.dart +++ b/dio/test/upload_stream_test.dart @@ -6,7 +6,7 @@ import 'package:test/test.dart'; void main() { var dio = Dio(); - dio.options.baseUrl = 'http://httpbin.org/'; + dio.options.baseUrl = 'https://httpbin.org/'; test('stream', () async { Response r; const str = 'hello 😌'; @@ -39,7 +39,7 @@ void main() { ); var img = base64Encode(f.readAsBytesSync()); expect(r.data['data'], 'data:application/octet-stream;base64,' + img); - }); + }, testOn: "vm"); test('file stream', () async { var f = File('../dio/test/test.jpg'); @@ -55,5 +55,5 @@ void main() { ); var img = base64Encode(f.readAsBytesSync()); expect(r.data['data'], 'data:application/octet-stream;base64,' + img); - }); + }, testOn: "vm"); } diff --git a/plugins/http2_adapter/lib/src/connection_manager_imp.dart b/plugins/http2_adapter/lib/src/connection_manager_imp.dart index 7175b06fe..82b2b56aa 100644 --- a/plugins/http2_adapter/lib/src/connection_manager_imp.dart +++ b/plugins/http2_adapter/lib/src/connection_manager_imp.dart @@ -79,10 +79,9 @@ class _ConnectionManager implements ConnectionManager { } on SocketException catch (e) { if (e.osError == null) { if (e.message.contains('timed out')) { - throw DioError( + throw DioError.connectionTimeout( + timeout: options.connectTimeout!, requestOptions: options, - error: 'Connecting timed out [${options.connectTimeout}]', - type: DioErrorType.connectTimeout, ); } }