Repository: cfug/dio

Issue:
Download causes Timeout exception
### Package

dio

### Version

5.3.3

### Operating-System

Android

### Output of `flutter doctor -v`

```shell
[✓] Flutter (Channel stable, 3.13.6, on macOS 13.5.2 22G91 darwin-arm64, locale de-DE)
    • Flutter version 3.13.6 on channel stable at /Users/patrick/development/sdks/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision ead455963c (8 weeks ago), 2023-09-26 18:28:17 -0700
    • Engine revision a794cf2681
    • Dart version 3.1.3
    • DevTools version 2.25.0

[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.2)
    • Android SDK at /Users/patrick/development/sdks/android
    • Platform android-34, build-tools 33.0.2
    • ANDROID_SDK_ROOT = /Users/patrick/development/sdks/android
    • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 17.0.6+0-17.0.6b802.4-9586694)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 15.0)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 15A240d
    • CocoaPods version 1.13.0

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2022.2)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 17.0.6+0-17.0.6b802.4-9586694)

[✓] IntelliJ IDEA Ultimate Edition (version 2023.2.1)
    • IntelliJ at /Applications/IntelliJ IDEA.app
    • Flutter plugin version 76.0.4
    • Dart plugin version 232.9559.10

[✓] VS Code (version 1.84.2)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension can be installed from:
      🔨 https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter

[✓] Connected device (4 available)
    • sdk gphone64 arm64 (mobile)         • emulator-5554             • android-arm64  • Android 12 (API 32) (emulator)
    • iPhone von Patrick Schmidt (mobile) • 00008110-000449CE2699801E • ios            • iOS 17.1.1 21B91
    • macOS (desktop)                     • macos                     • darwin-arm64   • macOS 13.5.2 22G91 darwin-arm64
    • Chrome (web)                        • chrome                    • web-javascript • Google Chrome 119.0.6045.159

[✓] Network resources
    • All expected network resources are available.
• No issues found!
```


### Dart Version

 3.1.3 

### Steps to Reproduce

1. Create a `Dio.download()` request with a `receiveTimeout` set that will trigger (In my case the download takes 10s so I set it to 3s).
2. Start downloading the file.

### Expected Result

As stated in the readme and the code itself, dio should only throw a `TimeoutException` based on the `receiveTimeout` defined "timeout during data transfer of each bytes and not the overall timing during the receiving.". 

Therefore, the download should finish without any issue since bytes are constantly received.

### Actual Result

The download is stopped due to a `TimeoutException` of the `receiveTimeout`. However, using the `onReceiveProgress` callback of the download method, I can see that progress and therefore bytes are received constantly.
receiveTimeout not working on iOS
### Package

dio

### Version

5.0.0

### Output of `flutter doctor -v`

```shell
[✓] Flutter (Channel stable, 3.7.7, on macOS 13.2.1 22D68 darwin-arm64, locale en-IT)
    • Flutter version 3.7.7 on channel stable at /Users/alessandro/development/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 2ad6cd72c0 (6 days ago), 2023-03-08 09:41:59 -0800
    • Engine revision 1837b5be5f
    • Dart version 2.19.4
    • DevTools version 2.20.1

[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.1)
    • Android SDK at /Users/alessandro/Library/Android/sdk
    • Platform android-33, build-tools 33.0.1
    • Java binary at: /Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.15+0-b2043.56-8887301)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 14.2)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 14C18
    • CocoaPods version 1.11.3

[✓] Chrome - develop for the web
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Android Studio (version 2022.1)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.15+0-b2043.56-8887301)

[✓] VS Code (version 1.75.0)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.58.0

[✓] Connected device (3 available)
    • iPhone 14 (mobile) • DF033D85-6295-4A8B-BA01-A8C885178B02 • ios            • com.apple.CoreSimulator.SimRuntime.iOS-16-2 (simulator)
    • macOS (desktop)    • macos                                • darwin-arm64   • macOS 13.2.1 22D68 darwin-arm64
    • Chrome (web)       • chrome                               • web-javascript • Google Chrome 111.0.5563.64

[✓] HTTP Host Availability
    • All required HTTP hosts are available

• No issues found!
```


### Dart Version

_No response_

### Steps to Reproduce

I'm using the following code at which I'm passing 30s as receivingTimeout but seems like has no effect on iOS because request don't get interrupted after that time. On Android instead it works successfully and it throws an exception.

 ```
Options options = Options(
        sendTimeout: Duration(milliseconds: connectionTimeoutInMillisec),
        receiveTimeout: Duration(milliseconds: receiveTimeoutInMilliSec),
        headers: headers,
        responseType: ResponseType.plain);
    Response? response;
    var dio = Dio();
    try {
      response = await dio.get(
        url,
        options: options,
      );
    }
```

### Expected Result

Request to be interrupted after X secs on iOS

### Actual Result

Request is not interrupted after X secs on iOS


Discussion: 
Thanks for reporting this issue.

Are you willing to submit a fix as a PR? We're happy to provide guidance.
@ueman 
I like to submit a pr to fix the issue?
To continue can you assign this issue to me & provider me guidance.
Can you send the error message in the actual result?that make it easier for traces issue.
@apaolino 

Fix patch:
diff --git a/.gitignore b/.gitignore
index 13c027fcd..79f775d20 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,8 +17,6 @@ dio/.pub/
 dio/.idea/
 dio/.exampl
 dio/coverage
-dio/test/_download_test.md
-dio/test/*_pinning.txt
 
 # plugins
 plugins/cookie_manager/.packages
diff --git a/dio/.gitignore b/dio/.gitignore
index cd69fa7c9..d9f19f740 100644
--- a/dio/.gitignore
+++ b/dio/.gitignore
@@ -30,3 +30,7 @@ build/
 
 # Exceptions to above rules.
 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
+
+# Project related.
+test/*_pinning.txt
+test/download/
diff --git a/dio/CHANGELOG.md b/dio/CHANGELOG.md
index 786414884..767fda138 100644
--- a/dio/CHANGELOG.md
+++ b/dio/CHANGELOG.md
@@ -7,6 +7,8 @@ See the [Migration Guide][] for the complete breaking changes list.**
 
 - Improve `SyncTransformer`'s stream transform.
 - Allow case-sensitive header keys with the `preserveHeaderCase` flag through options.
+- Fix `receiveTimeout` for the `IOHttpClientAdapter`.
+- Fix `receiveTimeout` for the `download` method of `DioForNative`.
 
 ## 5.3.4
 
diff --git a/dio/README-ZH.md b/dio/README-ZH.md
index a3819f000..01448c25c 100644
--- a/dio/README-ZH.md
+++ b/dio/README-ZH.md
@@ -284,11 +284,32 @@ final response = await dio.request(
 下面是 `Options` 的配置项：
 
 ```dart
-/// 请求方式。
+/// HTTP 请求方法。
 String method;
 
-/// 请求基本地址，可以包含路径例如 https://dart.dev/api/。
-String? baseUrl;
+/// 发送数据的超时设置。
+///
+/// 超时时会抛出类型为 [DioExceptionType.sendTimeout] 的
+/// [DioException]。
+///
+/// `null` 或 `Duration.zero` 即不设置超时。
+Duration? sendTimeout;
+
+/// 接收数据的超时设置。
+///
+/// 这里的超时对应的时间是：
+///  - 在建立连接和第一次收到响应数据事件之前的超时。
+///  - 每个数据事件传输的间隔时间，而不是接收的总持续时间。
+///
+/// 超时时会抛出类型为 [DioExceptionType.receiveTimeout] 的
+/// [DioException]。
+///
+/// `null` 或 `Duration.zero` 即不设置超时。
+Duration? receiveTimeout;
+
+/// 可以在 [Interceptor]、[Transformer] 和
+/// [Response.requestOptions] 中获取到的自定义对象。
+Map<String, dynamic>? extra;
 
 /// HTTP 请求头。
 ///
@@ -305,49 +326,62 @@ Map<String, dynamic>? headers;
 ///  - 按照 HTTP/2 的标准，只支持小写请求头键。
 bool? preserveHeaderCase;
 
-/// 连接服务器超时时间.
-Duration? connectTimeout;
-
-/// 两次数据流数据接收的最长间隔时间，注意不是请求的最长接收时间。
-Duration? receiveTimeout;
+/// 表示 [Dio] 处理请求响应数据的类型。
+///
+/// 默认值为 [ResponseType.json]。
+/// [Dio] 会在请求响应的 content-type
+/// 为 [Headers.jsonContentType] 时自动将响应字符串处理为 JSON 对象。
+///
+/// 在以下情况时，分别使用：
+///  - `plain` 将数据处理为 `String`；
+///  - `bytes` 将数据处理为完整的 bytes。
+///  - `stream` 将数据处理为流式返回的二进制数据；
+ResponseType? responseType;
 
-/// 请求内容体，可以是任意类型。
-dynamic data;
+/// 请求的 content-type。
+///
+/// 请求默认的 `content-type` 会由 [ImplyContentTypeInterceptor]
+/// 根据发送数据的类型推断。它可以通过
+/// [Interceptors.removeImplyContentTypeInterceptor] 移除。
+String? contentType;
 
-/// 请求路径，如果以 http(s)开始, 则 [baseURL] 会被忽略，
-/// 否则将会和 [baseUrl] 拼接出完整的地址。
-String path = '';
+/// 判断当前返回的状态码是否可以视为请求成功。
+ValidateStatus? validateStatus;
 
-/// 请求的 Content-Type。
-///
-/// 默认值会由 [ImplyContentTypeInterceptor] 根据请求载荷类型进行推导。
-/// 可以调用 [Interceptors.removeImplyContentTypeInterceptor] 进行移除。
+/// 是否在请求失败时仍然获取返回数据内容。
 ///
-/// 如果你想以 `application/x-www-form-urlencoded` 格式编码请求数据,
-/// 可以设置此选项为 `Headers.formUrlEncodedContentType`,
-/// [Dio] 会自动编码请求体。
-String? contentType;
+/// 默认为 true。
+bool? receiveDataWhenStatusError;
 
-/// 期望以哪种格式（方式）接受响应数据，包括 `json`、`stream` 和 `plain`。
+/// 参考 [HttpClientRequest.followRedirects]。
 ///
-/// 默认值是 `json`, 当响应头中 content-type 为 `application/json` 时，
-/// dio 会自动将响应内容转化为 json 对象。
-/// 如果想以二进制方式接受响应数据，如下载一个二进制文件，那么可以使用 `stream`。
+/// 默认为 true。
+bool? followRedirects;
+
+/// 当 [followRedirects] 为 true 时，指定的最大重定向次数。
+/// 如果请求超出了重定向次数上线，会抛出 [RedirectException]。
 ///
-/// 如果想以文本（字符串）格式接收响应数据，请使用 `plain`。
-ResponseType? responseType;
+/// 默认为 5。
+int? maxRedirects;
 
-/// `validateStatus` 决定 HTTP 响应状态码是否被视为请求成功，
-/// 返回 `true` 请求结果就会按成功处理，否则会按失败处理.
-ValidateStatus? validateStatus;
+/// 参考 [HttpClientRequest.persistentConnection]。
+///
+/// 默认为 true。
+bool? persistentConnection;
 
-/// 用户自定义字段，可以在 [Interceptor]、[Transformer] 和 [Response] 中依次传递。
-Map<String, dynamic>? extra;
+/// 对请求内容进行自定义编码转换。
+///
+/// 默认为 [Utf8Encoder]。
+RequestEncoder? requestEncoder;
 
-/// 请求地址的参数。
-Map<String, dynamic /*String|Iterable<String>*/ >? queryParameters;
+/// 对请求响应内容进行自定义解码转换。
+///
+/// 默认为 [Utf8Decoder]。
+ResponseDecoder? responseDecoder;
 
-/// 请求数据中数组的编码的方式，默认值为 `multiCompatible`。
+/// 当请求参数以 `x-www-url-encoded` 方式发送时，如何处理集合参数。
+///
+/// 默认为 [ListFormat.multi]。
 ListFormat? listFormat;
 ```
 
diff --git a/dio/README.md b/dio/README.md
index 4a268d1c2..b61253252 100644
--- a/dio/README.md
+++ b/dio/README.md
@@ -262,21 +262,44 @@ final response = await dio.request(
 
 ### Request Options
 
-The `Options` class describes the http request information and configuration.
+The `Options` class describes the HTTP request information and configuration.
 Each Dio instance has a base config for all requests made by itself,
 and we can override the base config with `Options` when make a single request.
 The `Options` declaration as follows:
 
 ```dart
-/// Http method.
+/// The HTTP request method.
 String method;
 
-/// Request base url, it can contain sub path, like: https://dart.dev/api/.
-String? baseUrl;
+/// Timeout when sending data.
+///
+/// Throws the [DioException] with
+/// [DioExceptionType.sendTimeout] type when timed out.
+///
+/// `null` or `Duration.zero` means no timeout limit.
+Duration? sendTimeout;
 
-/// Http request headers.
+/// Timeout when receiving data.
+///
+/// The timeout represents:
+///  - a timeout before the connection is established
+///    and the first received response bytes.
+///  - the duration during data transfer of each byte event,
+///    rather than the total duration of the receiving.
+///
+/// Throws the [DioException] with
+/// [DioExceptionType.receiveTimeout] type when timed out.
 ///
-/// The equality of the header keys is case-insensitive,
+/// `null` or `Duration.zero` means no timeout limit.
+Duration? receiveTimeout;
+
+/// Custom field that you can retrieve it later in [Interceptor],
+/// [Transformer] and the [Response.requestOptions] object.
+Map<String, dynamic>? extra;
+
+/// HTTP request headers.
+///
+/// The keys of the header are case-insensitive,
 /// e.g.: `content-type` and `Content-Type` will be treated as the same key.
 Map<String, dynamic>? headers;
 
@@ -289,59 +312,64 @@ Map<String, dynamic>? headers;
 /// - The HTTP/2 standard only supports lowercase header keys.
 bool? preserveHeaderCase;
 
-/// Timeout for opening url.
-Duration? connectTimeout;
-
-/// Whenever more than [receiveTimeout] passes between two events from response stream,
-/// [Dio] will throw the [DioException] with [DioExceptionType.RECEIVE_TIMEOUT].
-/// Note: This is not the receiving time limitation.
-Duration? receiveTimeout;
-
-/// Request data, can be any type.
-dynamic data;
-
-/// If the `path` starts with 'http(s)', the `baseURL` will be ignored, otherwise,
-/// it will be combined and then resolved with the baseUrl.
-String path;
+/// The type of data that [Dio] handles with options.
+///
+/// The default value is [ResponseType.json].
+/// [Dio] will parse response string to JSON object automatically
+/// when the content-type of response is [Headers.jsonContentType].
+///
+/// See also:
+///  - `plain` if you want to receive the data as `String`.
+///  - `bytes` if you want to receive the data as the complete bytes.
+///  - `stream` if you want to receive the data as streamed binary bytes.
+ResponseType? responseType;
 
-/// The request Content-Type.
+/// The request content-type.
 ///
 /// The default `content-type` for requests will be implied by the
 /// [ImplyContentTypeInterceptor] according to the type of the request payload.
 /// The interceptor can be removed by
 /// [Interceptors.removeImplyContentTypeInterceptor].
-///
-/// If you want to encode request body with 'application/x-www-form-urlencoded',
-/// you can set [Headers.formUrlEncodedContentType], and [Dio]
-/// will automatically encode the request body.
 String? contentType;
 
-/// [responseType] indicates the type of data that the server will respond with
-/// options which defined in [ResponseType] are `json`, `stream`, `plain`.
+/// Defines whether the request is considered to be successful
+/// with the given status code.
+/// The request will be treated as succeed if the callback returns true.
+ValidateStatus? validateStatus;
+
+/// Whether to retrieve the data if status code indicates a failed request.
 ///
-/// The default value is `json`, dio will parse response string to json object automatically
-/// when the content-type of response is 'application/json'.
+/// Defaults to true.
+bool? receiveDataWhenStatusError;
+
+/// See [HttpClientRequest.followRedirects].
 ///
-/// If you want to receive response data with binary bytes, for example,
-/// downloading a image, use `stream`.
+/// Defaults to true.
+bool? followRedirects;
+
+/// The maximum number of redirects when [followRedirects] is `true`.
+/// [RedirectException] will be thrown if redirects exceeded the limit.
 ///
-/// If you want to receive the response data with String, use `plain`.
-ResponseType? responseType;
+/// Defaults to 5.
+int? maxRedirects;
 
-/// `validateStatus` defines whether the request is successful for a given
-/// HTTP response status code. If `validateStatus` returns `true` ,
-/// the request will be perceived as successful; otherwise, considered as failed.
-ValidateStatus? validateStatus;
+/// See [HttpClientRequest.persistentConnection].
+///
+/// Defaults to true.
+bool? persistentConnection;
 
-/// Custom field that you can retrieve it later in [Interceptor],
-/// [Transformer] and the [Response.requestOptions] object.
-Map<String, dynamic>? extra;
+/// The default request encoder is [Utf8Encoder], you can set custom
+/// encoder by this option.
+RequestEncoder? requestEncoder;
 
-/// Common query parameters.
-Map<String, dynamic /*String|Iterable<String>*/ >? queryParameters;
+/// The default response decoder is [Utf8Decoder], you can set custom
+/// decoder by this option, it will be used in [Transformer].
+ResponseDecoder? responseDecoder;
 
-/// [listFormat] indicates the format of collection data in request options。
-/// The default value is `multiCompatible`
+/// Indicates the format of collection data in request query parameters and
+/// `x-www-url-encoded` body data.
+///
+/// Defaults to [ListFormat.multi].
 ListFormat? listFormat;
 ```
 
diff --git a/dio/lib/src/adapters/browser_adapter.dart b/dio/lib/src/adapters/browser_adapter.dart
index a4601ca52..cae27d089 100644
--- a/dio/lib/src/adapters/browser_adapter.dart
+++ b/dio/lib/src/adapters/browser_adapter.dart
@@ -161,20 +161,27 @@ class BrowserHttpClientAdapter implements HttpClientAdapter {
       }
     }
 
-    final downloadStopwatch = Stopwatch();
-    xhr.onProgress.listen((event) {
-      if (connectTimeoutTimer != null) {
-        connectTimeoutTimer!.cancel();
-        connectTimeoutTimer = null;
-      }
+    final receiveStopwatch = Stopwatch();
+    Timer? receiveTimer;
 
-      if (receiveTimeout > Duration.zero) {
-        if (!downloadStopwatch.isRunning) {
-          downloadStopwatch.start();
-        }
-        final duration = downloadStopwatch.elapsed;
-        if (duration > receiveTimeout) {
-          downloadStopwatch.stop();
+    void stopWatchReceiveTimeout() {
+      receiveTimer?.cancel();
+      receiveTimer = null;
+      receiveStopwatch.stop();
+    }
+
+    void watchReceiveTimeout() {
+      if (receiveTimeout <= Duration.zero) {
+        return;
+      }
+      receiveStopwatch.reset();
+      if (!receiveStopwatch.isRunning) {
+        receiveStopwatch.start();
+      }
+      receiveTimer?.cancel();
+      receiveTimer = Timer(receiveTimeout, () {
+        if (!completer.isCompleted) {
+          xhr.abort();
           completer.completeError(
             DioException.receiveTimeout(
               timeout: receiveTimeout,
@@ -182,16 +189,26 @@ class BrowserHttpClientAdapter implements HttpClientAdapter {
             ),
             StackTrace.current,
           );
-          xhr.abort();
         }
-      }
+        stopWatchReceiveTimeout();
+      });
+    }
 
-      if (options.onReceiveProgress != null) {
-        if (event.loaded != null && event.total != null) {
+    xhr.onProgress.listen(
+      (ProgressEvent event) {
+        if (connectTimeoutTimer != null) {
+          connectTimeoutTimer!.cancel();
+          connectTimeoutTimer = null;
+        }
+        watchReceiveTimeout();
+        if (options.onReceiveProgress != null &&
+            event.loaded != null &&
+            event.total != null) {
           options.onReceiveProgress!(event.loaded!, event.total!);
         }
-      }
-    });
+      },
+      onDone: () => stopWatchReceiveTimeout(),
+    );
 
     xhr.onError.first.then((_) {
       connectTimeoutTimer?.cancel();
diff --git a/dio/lib/src/adapters/io_adapter.dart b/dio/lib/src/adapters/io_adapter.dart
index 5c7d94ece..85de89359 100644
--- a/dio/lib/src/adapters/io_adapter.dart
+++ b/dio/lib/src/adapters/io_adapter.dart
@@ -168,13 +168,13 @@ class IOHttpClientAdapter implements HttpClientAdapter {
       await future;
     }
 
-    final stopwatch = Stopwatch()..start();
     Future<HttpClientResponse> future = request.close();
-    final receiveTimeout = options.receiveTimeout;
-    if (receiveTimeout != null && receiveTimeout > Duration.zero) {
+    final receiveTimeout = options.receiveTimeout ?? Duration.zero;
+    if (receiveTimeout > Duration.zero) {
       future = future.timeout(
         receiveTimeout,
         onTimeout: () {
+          request.abort();
           throw DioException.receiveTimeout(
             timeout: receiveTimeout,
             requestOptions: options,
@@ -182,7 +182,6 @@ class IOHttpClientAdapter implements HttpClientAdapter {
         },
       );
     }
-
     final responseStream = await future;
 
     if (validateCertificate != null) {
@@ -203,26 +202,50 @@ class IOHttpClientAdapter implements HttpClientAdapter {
       }
     }
 
+    final receiveStopwatch = Stopwatch();
+    Timer? receiveTimer;
+
+    void stopWatchReceiveTimeout() {
+      receiveTimer?.cancel();
+      receiveTimer = null;
+      receiveStopwatch.stop();
+    }
+
+    void watchReceiveTimeout(EventSink<Uint8List> sink) {
+      if (receiveTimeout <= Duration.zero) {
+        return;
+      }
+      receiveStopwatch.reset();
+      if (!receiveStopwatch.isRunning) {
+        receiveStopwatch.start();
+      }
+      receiveTimer?.cancel();
+      receiveTimer = Timer(receiveTimeout, () {
+        sink.addError(
+          DioException.receiveTimeout(
+            timeout: receiveTimeout,
+            requestOptions: options,
+          ),
+        );
+        sink.close();
+        responseStream.detachSocket().then((socket) => socket.destroy());
+        stopWatchReceiveTimeout();
+      });
+    }
+
     final stream = responseStream.transform<Uint8List>(
       StreamTransformer.fromHandlers(
         handleData: (data, sink) {
-          stopwatch.stop();
-          final duration = stopwatch.elapsed;
-          final receiveTimeout = options.receiveTimeout;
-          if (receiveTimeout != null &&
-              receiveTimeout > Duration.zero &&
-              duration > receiveTimeout) {
-            sink.addError(
-              DioException.receiveTimeout(
-                timeout: receiveTimeout,
-                requestOptions: options,
-              ),
-            );
-            responseStream.detachSocket().then((socket) => socket.destroy());
-          } else {
-            sink.add(Uint8List.fromList(data));
+          watchReceiveTimeout(sink);
+          // Always true if the receive timeout was not set.
+          if (receiveStopwatch.elapsed <= receiveTimeout) {
+            sink.add(data is Uint8List ? data : Uint8List.fromList(data));
           }
         },
+        handleDone: (sink) {
+          stopWatchReceiveTimeout();
+          sink.close();
+        },
       ),
     );
 
@@ -245,8 +268,11 @@ class IOHttpClientAdapter implements HttpClientAdapter {
 
   HttpClient _configHttpClient(Duration? connectionTimeout) {
     _cachedHttpClient ??= _createHttpClient();
-    if (connectionTimeout != null && connectionTimeout > Duration.zero) {
+    connectionTimeout ??= Duration.zero;
+    if (connectionTimeout > Duration.zero) {
       _cachedHttpClient!.connectionTimeout = connectionTimeout;
+    } else {
+      _cachedHttpClient!.connectionTimeout = null;
     }
     return _cachedHttpClient!;
   }
diff --git a/dio/lib/src/dio/dio_for_native.dart b/dio/lib/src/dio/dio_for_native.dart
index d16fa4124..a725abdec 100644
--- a/dio/lib/src/dio/dio_for_native.dart
+++ b/dio/lib/src/dio/dio_for_native.dart
@@ -36,13 +36,12 @@ class DioForNative with DioMixin implements Dio {
     Object? data,
     Options? options,
   }) async {
-    // We set the `responseType` to [ResponseType.STREAM] to retrieve the
-    // response stream.
     options ??= DioMixin.checkOptions('GET', options);
-
-    // Receive data with stream.
-    options.responseType = ResponseType.stream;
-    Response<ResponseBody> response;
+    // Manually set the `responseType` to [ResponseType.stream]
+    // to retrieve the response stream.
+    // Do not modify previous options.
+    options = options.copyWith(responseType: ResponseType.stream);
+    final Response<ResponseBody> response;
     try {
       response = await request<ResponseBody>(
         urlPath,
@@ -53,14 +52,22 @@ class DioForNative with DioMixin implements Dio {
       );
     } on DioException catch (e) {
       if (e.type == DioExceptionType.badResponse) {
-        if (e.response!.requestOptions.receiveDataWhenStatusError == true) {
+        final response = e.response!;
+        if (response.requestOptions.receiveDataWhenStatusError == true) {
+          final ResponseType implyResponseType;
+          final contentType = response.headers.value(Headers.contentTypeHeader);
+          if (contentType != null && contentType.startsWith('text/')) {
+            implyResponseType = ResponseType.plain;
+          } else {
+            implyResponseType = ResponseType.json;
+          }
           final res = await transformer.transformResponse(
-            e.response!.requestOptions..responseType = ResponseType.json,
-            e.response!.data as ResponseBody,
+            response.requestOptions.copyWith(responseType: implyResponseType),
+            response.data as ResponseBody,
           );
-          e.response!.data = res;
+          response.data = res;
         } else {
-          e.response!.data = null;
+          response.data = null;
         }
       }
       rethrow;
@@ -92,7 +99,6 @@ class DioForNative with DioMixin implements Dio {
 
     // Create a Completer to notify the success/error state.
     final completer = Completer<Response>();
-    Future<Response> future = completer.future;
     int received = 0;
 
     // Stream<Uint8List>
@@ -139,7 +145,7 @@ class DioForNative with DioMixin implements Dio {
           }
         }).catchError((Object e) async {
           try {
-            await subscription.cancel();
+            await subscription.cancel().catchError((_) {});
             closed = true;
             await raf.close().catchError((_) => raf);
             if (deleteOnError && file.existsSync()) {
@@ -179,25 +185,6 @@ class DioForNative with DioMixin implements Dio {
       await subscription.cancel();
       await closeAndDelete();
     });
-
-    final timeout = response.requestOptions.receiveTimeout;
-    if (timeout != null) {
-      future = future.timeout(timeout).catchError(
-        (dynamic e, StackTrace s) async {
-          await subscription.cancel();
-          await closeAndDelete();
-          if (e is TimeoutException) {
-            throw DioException.receiveTimeout(
-              timeout: timeout,
-              requestOptions: response.requestOptions,
-              error: e,
-            );
-          } else {
-            throw e;
-          }
-        },
-      );
-    }
-    return DioMixin.listenCancelForAsyncTask(cancelToken, future);
+    return DioMixin.listenCancelForAsyncTask(cancelToken, completer.future);
   }
 }
diff --git a/dio/lib/src/dio_exception.dart b/dio/lib/src/dio_exception.dart
index b39fff05c..a5bd2d404 100644
--- a/dio/lib/src/dio_exception.dart
+++ b/dio/lib/src/dio_exception.dart
@@ -133,7 +133,7 @@ class DioException implements Exception {
       DioException(
         type: DioExceptionType.receiveTimeout,
         message: 'The request took longer than $timeout to receive data. '
-            'It was aborted.'
+            'It was aborted. '
             'To get rid of this exception, try raising the '
             'RequestOptions.receiveTimeout above the duration of $timeout or '
             'improve the response time of the server.',
diff --git a/dio/lib/src/options.dart b/dio/lib/src/options.dart
index b95994f7c..8a816ae98 100644
--- a/dio/lib/src/options.dart
+++ b/dio/lib/src/options.dart
@@ -84,7 +84,7 @@ typedef ResponseDecoder = FutureOr<String?> Function(
   ResponseBody responseBody,
 );
 
-/// The type of a response encoding callback.
+/// The type of a request encoding callback.
 typedef RequestEncoder = FutureOr<List<int>> Function(
   String request,
   RequestOptions options,
@@ -100,14 +100,14 @@ mixin OptionsMixin {
   /// List values use the default [ListFormat.multiCompatible].
   /// The value can be overridden per parameter by adding a [ListParam]
   /// object wrapping the actual List value and the desired format.
-  late Map<String, dynamic> queryParameters;
+  late Map<String, dynamic /*String|Iterable<String>*/ > queryParameters;
 
-  /// Timeout when opening url.
+  /// Timeout when opening a request.
   ///
-  /// [Dio] will throw the [DioException] with
+  /// Throws the [DioException] with
   /// [DioExceptionType.connectionTimeout] type when time out.
   ///
-  /// `null` or `Duration.zero` meanings no timeout limit.
+  /// `null` or `Duration.zero` means no timeout limit.
   Duration? get connectTimeout => _connectTimeout;
   Duration? _connectTimeout;
 
@@ -362,7 +362,7 @@ class Options {
 
   /// HTTP request headers.
   ///
-  /// The equality of the header keys is case-insensitive,
+  /// The keys of the header are case-insensitive,
   /// e.g.: `content-type` and `Content-Type` will be treated as the same key.
   Map<String, dynamic>? headers;
 
@@ -377,10 +377,10 @@ class Options {
 
   /// Timeout when sending data.
   ///
-  /// [Dio] will throw the [DioException] with
+  /// Throws the [DioException] with
   /// [DioExceptionType.sendTimeout] type when timed out.
   ///
-  /// `null` or `Duration.zero` meanings no timeout limit.
+  /// `null` or `Duration.zero` means no timeout limit.
   Duration? get sendTimeout => _sendTimeout;
   Duration? _sendTimeout;
 
@@ -393,13 +393,16 @@ class Options {
 
   /// Timeout when receiving data.
   ///
-  /// The timeout represents the timeout during data transfer of each bytes,
-  /// rather than the overall timing during the receiving.
+  /// The timeout represents:
+  ///  - a timeout before the connection is established
+  ///    and the first received response bytes.
+  ///  - the duration during data transfer of each byte event,
+  ///    rather than the total duration of the receiving.
   ///
-  /// [Dio] will throw the [DioException] with
-  /// [DioExceptionType.receiveTimeout] type when time out.
+  /// Throws the [DioException] with
+  /// [DioExceptionType.receiveTimeout] type when timed out.
   ///
-  /// `null` or `Duration.zero` meanings no timeout limit.
+  /// `null` or `Duration.zero` means no timeout limit.
   Duration? get receiveTimeout => _receiveTimeout;
   Duration? _receiveTimeout;
 
@@ -415,20 +418,20 @@ class Options {
   /// {@macro dio.interceptors.ImplyContentTypeInterceptor}
   String? contentType;
 
-  /// Indicates the type of data that the server will respond with options.
+  /// The type of data that [Dio] handles with options.
   ///
-  /// The default value is [ResponseType.json], [Dio] will parse response string
-  /// to JSON object automatically when the content-type of response is
-  /// [Headers.jsonContentType].
+  /// The default value is [ResponseType.json].
+  /// [Dio] will parse response string to JSON object automatically
+  /// when the content-type of response is [Headers.jsonContentType].
   ///
-  /// If you want to receive response data with binary bytes, use `stream`.
-  ///
-  /// If you want to receive the response data with String, use `plain`.
-  ///
-  /// If you want to receive the response data with original bytes, use `bytes`.
+  /// See also:
+  ///  - `plain` if you want to receive the data as `String`.
+  ///  - `bytes` if you want to receive the data as the complete bytes.
+  ///  - `stream` if you want to receive the data as streamed binary bytes.
   ResponseType? responseType;
 
-  /// Defines whether the request is succeed with the given status code.
+  /// Defines whether the request is considered to be successful
+  /// with the given status code.
   /// The request will be treated as succeed if the callback returns true.
   ValidateStatus? validateStatus;
 
@@ -459,12 +462,14 @@ class Options {
   /// Defaults to true.
   bool? persistentConnection;
 
-  /// The default request encoder is utf8encoder, you can set custom
-  /// encoder by this option.
+  /// The type of a request encoding callback.
+  ///
+  /// Defaults to [Utf8Encoder].
   RequestEncoder? requestEncoder;
 
-  /// The default response decoder is utf8decoder, you can set custom
-  /// decoder by this option, it will be used in [Transformer].
+  /// The type of a response decoding callback.
+  ///
+  /// Defaults to [Utf8Decoder].
   ResponseDecoder? responseDecoder;
 
   /// Indicates the format of collection data in request query parameters and
diff --git a/plugins/http2_adapter/CHANGELOG.md b/plugins/http2_adapter/CHANGELOG.md
index 960fab0cf..1b6622feb 100644
--- a/plugins/http2_adapter/CHANGELOG.md
+++ b/plugins/http2_adapter/CHANGELOG.md
@@ -3,6 +3,7 @@
 ## Unreleased
 
 - Support non-TLS connection requests.
+- Improve the implementation of `receiveTimeout`.
 
 ## 2.3.2
 
diff --git a/plugins/http2_adapter/lib/src/http2_adapter.dart b/plugins/http2_adapter/lib/src/http2_adapter.dart
index 0c57d62da..b27b84745 100644
--- a/plugins/http2_adapter/lib/src/http2_adapter.dart
+++ b/plugins/http2_adapter/lib/src/http2_adapter.dart
@@ -1,3 +1,4 @@
+// ignore_for_file: void_checks
 import 'dart:async';
 import 'dart:convert';
 import 'dart:io';
@@ -89,35 +90,68 @@ class Http2Adapter implements HttpClientAdapter {
     }
 
     if (hasRequestData) {
-      final requestStreamFuture = requestStream!.listen((data) {
+      Future<void> requestStreamFuture = requestStream!.listen((data) {
         stream.outgoingMessages.add(DataStreamMessage(data));
       }).asFuture();
       final sendTimeout = options.sendTimeout ?? Duration.zero;
       if (sendTimeout > Duration.zero) {
-        await requestStreamFuture.timeout(
+        requestStreamFuture = requestStreamFuture.timeout(
           sendTimeout,
           onTimeout: () {
+            stream.terminate();
             throw DioException.sendTimeout(
               timeout: sendTimeout,
               requestOptions: options,
             );
           },
         );
-      } else {
-        await requestStreamFuture;
       }
+      await requestStreamFuture;
     }
     await stream.outgoingMessages.close();
 
     final sc = StreamController<Uint8List>();
     final responseHeaders = Headers();
-    final completer = Completer();
-    late int statusCode;
+    final responseCompleter = Completer<void>();
     bool needRedirect = false;
-    late StreamSubscription subscription;
     bool needResponse = false;
+
+    final receiveTimeout = options.receiveTimeout ?? Duration.zero;
+    final receiveStopwatch = Stopwatch();
+    Timer? receiveTimer;
+
+    void stopWatchReceiveTimeout() {
+      receiveTimer?.cancel();
+      receiveTimer = null;
+      receiveStopwatch.stop();
+    }
+
+    void watchReceiveTimeout() {
+      if (receiveTimeout <= Duration.zero) {
+        return;
+      }
+      receiveStopwatch.reset();
+      if (!receiveStopwatch.isRunning) {
+        receiveStopwatch.start();
+      }
+      receiveTimer?.cancel();
+      receiveTimer = Timer(receiveTimeout, () {
+        sc.addError(
+          DioException.receiveTimeout(
+            timeout: receiveTimeout,
+            requestOptions: options,
+          ),
+        );
+        sc.close();
+        stream.terminate();
+        stopWatchReceiveTimeout();
+      });
+    }
+
+    late int statusCode;
+    late StreamSubscription subscription;
     subscription = stream.incomingMessages.listen(
-      (message) async {
+      (StreamMessage message) async {
         if (message is HeadersStreamMessage) {
           for (final header in message.headers) {
             final name = utf8.decode(header.name);
@@ -133,34 +167,47 @@ class Http2Adapter implements HttpClientAdapter {
             needResponse =
                 !needRedirect && options.validateStatus(statusCode) ||
                     options.receiveDataWhenStatusError;
-            completer.complete();
+            responseCompleter.complete();
           }
         } else if (message is DataStreamMessage) {
           if (needResponse) {
-            sc.add(Uint8List.fromList(message.bytes));
+            watchReceiveTimeout();
+            sc.add(
+              message.bytes is Uint8List
+                  ? message.bytes as Uint8List
+                  : Uint8List.fromList(message.bytes),
+            );
           } else {
-            subscription.cancel().whenComplete(() => sc.close());
+            stopWatchReceiveTimeout();
+            subscription.cancel().whenComplete(() {
+              stream.terminate();
+              sc.close();
+            });
           }
         }
       },
-      onDone: () => sc.close(),
+      onDone: () {
+        stopWatchReceiveTimeout();
+        sc.close();
+      },
       onError: (Object error, StackTrace stackTrace) {
-        // If connection is being forcefully terminated, remove the connection
+        // If connection is being forcefully terminated, remove the connection.
         if (error is TransportConnectionException) {
           _connectionMgr.removeConnection(transport);
         }
-        if (!completer.isCompleted) {
-          completer.completeError(error, stackTrace);
+        if (!responseCompleter.isCompleted) {
+          responseCompleter.completeError(error, stackTrace);
         } else {
           sc.addError(error, stackTrace);
         }
+        stopWatchReceiveTimeout();
       },
       cancelOnError: true,
     );
 
-    final receiveTimeout = options.receiveTimeout ?? Duration.zero;
+    Future<void> responseFuture = responseCompleter.future;
     if (receiveTimeout > Duration.zero) {
-      await completer.future.timeout(
+      responseFuture = responseFuture.timeout(
         receiveTimeout,
         onTimeout: () {
           subscription.cancel().whenComplete(() => sc.close());
@@ -170,11 +217,10 @@ class Http2Adapter implements HttpClientAdapter {
           );
         },
       );
-    } else {
-      await completer.future;
     }
+    await responseFuture;
 
-    // Handle redirection
+    // Handle redirection.
     if (needRedirect) {
       final url = responseHeaders.value('location');
       redirects.add(


Test patch:
diff --git a/dio/test/download_test.dart b/dio/test/download_test.dart
index 9f341ab14..74ad399cb 100644
--- a/dio/test/download_test.dart
+++ b/dio/test/download_test.dart
@@ -11,8 +11,17 @@ import 'utils.dart';
 void main() {
   setUp(startServer);
   tearDown(stopServer);
+
+  test('download does not change the response type', () async {
+    const savePath = 'test/download/_test.md';
+    final dio = Dio()..options.baseUrl = serverUrl.toString();
+    final options = Options(responseType: ResponseType.plain);
+    await dio.download('/download', savePath, options: options);
+    expect(options.responseType, ResponseType.plain);
+  });
+
   test('download1', () async {
-    const savePath = 'test/_download_test.md';
+    const savePath = 'test/download/_test.md';
     final dio = Dio()..options.baseUrl = serverUrl.toString();
     await dio.download('/download', savePath);
 
@@ -22,7 +31,7 @@ void main() {
   });
 
   test('download2', () async {
-    const savePath = 'test/_download_test.md';
+    const savePath = 'test/download/_test.md';
     final dio = Dio()..options.baseUrl = serverUrl.toString();
     await dio.downloadUri(
       serverUrl.replace(path: '/download'),
@@ -35,7 +44,7 @@ void main() {
   });
 
   test('download error', () async {
-    const savePath = 'test/_download_test.md';
+    const savePath = 'test/download/_test.md';
     final dio = Dio()..options.baseUrl = serverUrl.toString();
     Response response = await dio
         .download('/error', savePath)
@@ -52,23 +61,35 @@ void main() {
   });
 
   test('download timeout', () async {
-    const savePath = 'test/_download_test.md';
-    final dio = Dio(
-      BaseOptions(
-        receiveTimeout: Duration(milliseconds: 1),
-        baseUrl: serverUrl.toString(),
+    final dio = Dio();
+    final timeoutMatcher = allOf([
+      throwsA(isA<DioException>()),
+      throwsA(
+        predicate<DioException>(
+          (e) => e.type == DioExceptionType.receiveTimeout,
+        ),
       ),
+    ]);
+    await expectLater(
+      dio.downloadUri(
+        Uri.parse('$serverUrl/download').replace(
+          queryParameters: {'count': '3', 'gap': '2'},
+        ),
+        'test/download/_test.md',
+        options: Options(receiveTimeout: Duration(seconds: 1)),
+      ),
+      timeoutMatcher,
     );
-    expect(
-      dio
-          .download('/download', savePath)
-          .catchError((e) => throw (e as DioException).type),
-      throwsA(DioExceptionType.receiveTimeout),
+    // Throws nothing if it constantly gets response bytes.
+    await dio.download(
+      'https://github.com/cfug/flutter.cn/archive/refs/heads/main.zip',
+      'test/download/main.zip',
+      options: Options(receiveTimeout: Duration(seconds: 1)),
     );
   });
 
   test('download cancellation', () async {
-    const savePath = 'test/_download_test.md';
+    const savePath = 'test/download/_test.md';
     final cancelToken = CancelToken();
     Future.delayed(Duration(milliseconds: 100), () {
       cancelToken.cancel();
@@ -86,7 +107,7 @@ void main() {
   });
 
   test('delete on error', () async {
-    const savePath = 'test/_download_test.md';
+    const savePath = 'test/download/_test.md';
     final f = File(savePath)..createSync(recursive: true);
     expect(f.existsSync(), isTrue);
 
@@ -106,7 +127,7 @@ void main() {
   });
 
   test('delete on cancel', () async {
-    const savePath = 'test/_download_test.md';
+    const savePath = 'test/download/_test.md';
     final f = File(savePath)..createSync(recursive: true);
     expect(f.existsSync(), isTrue);
 
diff --git a/dio/test/timeout_test.dart b/dio/test/timeout_test.dart
index 367b7fd5e..7626baade 100644
--- a/dio/test/timeout_test.dart
+++ b/dio/test/timeout_test.dart
@@ -12,7 +12,7 @@ void main() {
     dio.options.baseUrl = 'https://httpbun.com/';
   });
 
-  test('catch DioException when connect timeout', () {
+  test('catch DioException when connectTimeout', () {
     dio.options.connectTimeout = Duration(milliseconds: 3);
 
     expectLater(
@@ -26,23 +26,36 @@ void main() {
     );
   });
 
-  test('catch DioException when receiveTimeout', () {
-    dio.options.receiveTimeout = Duration(milliseconds: 10);
+  test('catch DioException when receiveTimeout', () async {
+    dio.options.receiveTimeout = Duration(seconds: 1);
 
-    expectLater(
+    final matcher = allOf([
+      throwsA(isA<DioException>()),
+      throwsA(
+        predicate<DioException>(
+          (e) => e.type == DioExceptionType.receiveTimeout,
+        ),
+      ),
+      throwsA(
+        predicate<DioException>((e) => e.message!.contains('0:00:01.000000')),
+      ),
+    ]);
+    await expectLater(
+      dio.get(
+        '/drip',
+        queryParameters: {'delay': 2},
+      ),
+      matcher,
+    );
+    await expectLater(
       dio.get(
-        '/bytes/${1024 * 1024 * 20}',
+        '/drip',
+        queryParameters: {'delay': 0, 'duration:': 1},
         options: Options(responseType: ResponseType.stream),
       ),
-      allOf([
-        throwsA(isA<DioException>()),
-        throwsA(predicate(
-            (DioException e) => e.type == DioExceptionType.receiveTimeout)),
-        throwsA(predicate(
-            (DioException e) => e.message!.contains('0:00:00.010000'))),
-      ]),
+      matcher,
     );
-  }, testOn: 'vm');
+  });
 
   test('no DioException when receiveTimeout > request duration', () async {
     dio.options.receiveTimeout = Duration(seconds: 5);
@@ -76,25 +89,51 @@ void main() {
       BaseOptions(
         baseUrl: 'https://httpbun.com/',
         connectTimeout: Duration.zero,
-        sendTimeout: Duration.zero,
         receiveTimeout: Duration.zero,
       ),
     );
     // Ignores zero duration timeouts from the base options.
     await dio.get('/drip-lines?delay=1');
     // Reset the base options.
-    dio.options
-      ..connectTimeout = Duration(seconds: 10)
-      ..sendTimeout = Duration(seconds: 10)
-      ..receiveTimeout = Duration(seconds: 10);
+    dio.options.receiveTimeout = Duration(milliseconds: 10);
+    await expectLater(
+      dio.get('/drip-lines?delay=1'),
+      allOf([
+        throwsA(isA<DioException>()),
+        throwsA(
+          predicate<DioException>(
+            (e) => e.type == DioExceptionType.receiveTimeout,
+          ),
+        ),
+        throwsA(
+          predicate<DioException>(
+            (e) => e.message!.contains('0:00:00.010000'),
+          ),
+        ),
+      ]),
+    );
+    dio.options.connectTimeout = Duration(milliseconds: 10);
+    await expectLater(
+      dio.get('/drip-lines?delay=1'),
+      allOf([
+        throwsA(isA<DioException>()),
+        throwsA(
+          predicate<DioException>(
+            (e) => e.type == DioExceptionType.connectionTimeout,
+          ),
+        ),
+        throwsA(
+          predicate<DioException>(
+            (e) => e.message!.contains('0:00:00.010000'),
+          ),
+        ),
+      ]),
+    );
+    dio.options.connectTimeout = Duration.zero;
     // Override with request options.
     await dio.get(
       '/drip-lines?delay=1',
-      options: Options(
-        sendTimeout: Duration.zero,
-        receiveTimeout: Duration.zero,
-      ),
+      options: Options(receiveTimeout: Duration.zero),
     );
-    await dio.get('/drip-lines?delay=1');
   });
 }
diff --git a/dio/test/utils.dart b/dio/test/utils.dart
index 90bd036da..fb4c0669e 100644
--- a/dio/test/utils.dart
+++ b/dio/test/utils.dart
@@ -91,14 +91,24 @@ Future<void> startServer() async {
       }
 
       if (path == '/download') {
+        final count = int.parse(request.uri.queryParameters['count'] ?? '1');
+        final gap = int.parse(request.uri.queryParameters['gap'] ?? '0');
         const content = 'I am a text file';
-        response.headers.set('content-encoding', 'plain');
+        final contentBytes = utf8.encode(content);
         response
-          ..statusCode = 200
-          ..contentLength = content.length
-          ..write(content);
-
-        Future.delayed(Duration(milliseconds: 300), () => response.close());
+          ..bufferOutput = gap <= 0
+          ..contentLength = contentBytes.length * count
+          ..headers.set('content-encoding', 'plain')
+          ..statusCode = 200;
+        for (int i = 0; i < count; i++) {
+          response.add(contentBytes);
+          await response.flush();
+          if (i < count - 1 && gap > 0) {
+            await Future.delayed(Duration(seconds: gap));
+          }
+        }
+        await Future.delayed(const Duration(milliseconds: 300));
+        await response.close();
         return;
       }
 
diff --git a/plugins/http2_adapter/test/http2_test.dart b/plugins/http2_adapter/test/http2_test.dart
index feb18b9b4..972558317 100644
--- a/plugins/http2_adapter/test/http2_test.dart
+++ b/plugins/http2_adapter/test/http2_test.dart
@@ -12,7 +12,7 @@ void main() {
 
   test('adds one to input values', () async {
     final dio = Dio()
-      ..options.baseUrl = 'https://pub.dev/'
+      ..options.baseUrl = 'https://httpbun.com/'
       ..interceptors.add(LogInterceptor())
       ..httpClientAdapter = Http2Adapter(
         ConnectionManager(
@@ -22,7 +22,7 @@ void main() {
       );
 
     Response<String> response;
-    response = await dio.get('?xx=6');
+    response = await dio.get('get');
     assert(response.statusCode == 200);
     response = await dio.get(
       'nkjnjknjn.html',
diff --git a/plugins/http2_adapter/test/timeout_test.dart b/plugins/http2_adapter/test/timeout_test.dart
new file mode 100644
index 000000000..507c5c201
--- /dev/null
+++ b/plugins/http2_adapter/test/timeout_test.dart
@@ -0,0 +1,61 @@
+@TestOn('vm')
+import 'package:dio/dio.dart';
+import 'package:dio_http2_adapter/dio_http2_adapter.dart';
+import 'package:test/test.dart';
+
+void main() {
+  late Dio dio;
+
+  setUp(() {
+    dio = Dio();
+    dio.options.baseUrl = 'https://httpbun.com/';
+    dio.httpClientAdapter = Http2Adapter(
+      ConnectionManager(idleTimeout: Duration(seconds: 30)),
+    );
+  });
+
+  test('catch DioException when connectTimeout', () {
+    dio.options.connectTimeout = Duration(milliseconds: 3);
+
+    expectLater(
+      dio.get('/drip-lines?delay=2'),
+      allOf(
+        throwsA(isA<DioException>()),
+        throwsA(predicate((DioException e) =>
+            e.type == DioExceptionType.connectionTimeout &&
+            e.message!.contains('0:00:00.003000'))),
+      ),
+    );
+  });
+
+  test('catch DioException when receiveTimeout', () async {
+    dio.options.receiveTimeout = Duration(seconds: 1);
+
+    final matcher = allOf([
+      throwsA(isA<DioException>()),
+      throwsA(
+        predicate<DioException>(
+          (e) => e.type == DioExceptionType.receiveTimeout,
+        ),
+      ),
+      throwsA(
+        predicate<DioException>((e) => e.message!.contains('0:00:01.000000')),
+      ),
+    ]);
+    await expectLater(
+      dio.get(
+        '/drip',
+        queryParameters: {'delay': 2},
+      ),
+      matcher,
+    );
+    await expectLater(
+      dio.get(
+        '/drip',
+        queryParameters: {'delay': 0, 'duration:': 1},
+        options: Options(responseType: ResponseType.stream),
+      ),
+      matcher,
+    );
+  });
+}