Repository: cfug/dio

Issue:
Download  receiveTimeout 
### Package

dio

### Version

5.3.3

### Operating-System

Android

### Output of `flutter doctor -v`

```shell
┌─────────────────────────────────────────────────────────┐
│ A new version of Flutter is available!                  │
│                                                         │
│ To update to the latest version, run "flutter upgrade". │
└─────────────────────────────────────────────────────────┘
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.13.6, on macOS 14.0 23A344 darwin-arm64, locale
    zh-Hans-CN)
[✓] Android toolchain - develop for Android devices (Android SDK version 32.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 15.0)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2022.3)
[✓] IntelliJ IDEA Ultimate Edition (version 2023.1.1)
[✓] VS Code (version 1.77.1)
[✓] Connected device (4 available)
```


### Dart Version

_No response_

### Steps to Reproduce

In previous versions, it was only necessary to set the receiveTimeout of the Download Options to 0 to download without timeout. Now, after changing to the Duration type, the time cannot be unlimited

```dart
Future downloadFile(urlPath, savePath,
      {CancelToken? cancelToken, ProgressCallback? onReceiveProgress}) async {
    Response? response;
    response = await dio.download(urlPath, savePath,
        cancelToken: cancelToken,
        options: Options(receiveTimeout: Duration(seconds: 0)),
        onReceiveProgress: onReceiveProgress ??
            (int count, int total) {
              logI("$count $total");
            });
    return response;
  }
```

### Expected Result

download success

### Actual Result

[ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: DioException [receive timeout]: The request took longer than 0:00:00.000000 to receive data. It was aborted.


Discussion: You should be able to set it to `null` now.
> You should be able to set it to 'null' now.

I have tried to set it to 'null', but it defaults to using the 'receiveTimeout' of the option for the single instance 'dio', so my current method can only set it to a relatively large time
Oh, right so you can not override it in the `Options` passed to a single request if you have a different duration set it in the `BaseOptions`?
You should be able to set it to `null` on the `BaseOptions`?

Do I understand this correctly?
> Oh, right so you can not override it in the `Options` passed to a single request if you have a different duration set it in the `BaseOptions`? You should be able to set it to `null` on the `BaseOptions`?
> 
> Do I understand this correctly?

Yes, Unless a new `Dio` is instantiated. For me, the entire app uses the same single instance `Dio`, and the `receiveTimeout` set by the Base Options is 15 seconds.
So the most straight-forward solution would be to ignore zero duration? @ueman
Yea I agree

Fix patch:
diff --git a/dio/CHANGELOG.md b/dio/CHANGELOG.md
index b361c1b93..7c05a1bff 100644
--- a/dio/CHANGELOG.md
+++ b/dio/CHANGELOG.md
@@ -6,8 +6,9 @@ See the [Migration Guide][] for the complete breaking changes list.**
 ## Unreleased
 
 - Raise warning for `Map`s other than `Map<String, dynamic>` when encoding request data.
-- Improve exception messages
-- Allow `ResponseDecoder` and `RequestEncoder` to be async
+- Improve exception messages.
+- Allow `ResponseDecoder` and `RequestEncoder` to be async.
+- Ignores `Duration.zero` timeouts.
 
 ## 5.3.3
 
diff --git a/dio/lib/src/adapters/browser_adapter.dart b/dio/lib/src/adapters/browser_adapter.dart
index 399113c76..a4601ca52 100644
--- a/dio/lib/src/adapters/browser_adapter.dart
+++ b/dio/lib/src/adapters/browser_adapter.dart
@@ -58,15 +58,11 @@ class BrowserHttpClientAdapter implements HttpClientAdapter {
       }
     });
 
-    final connectTimeout = options.connectTimeout;
-    final receiveTimeout = options.receiveTimeout;
-    int xhrTimeout = 0;
-    if (connectTimeout != null &&
-        receiveTimeout != null &&
-        receiveTimeout > Duration.zero) {
-      xhrTimeout = (connectTimeout + receiveTimeout).inMilliseconds;
-      xhr.timeout = xhrTimeout;
-    }
+    final sendTimeout = options.sendTimeout ?? Duration.zero;
+    final connectTimeout = options.connectTimeout ?? Duration.zero;
+    final receiveTimeout = options.receiveTimeout ?? Duration.zero;
+    final xhrTimeout = (connectTimeout + receiveTimeout).inMilliseconds;
+    xhr.timeout = xhrTimeout;
 
     final completer = Completer<ResponseBody>();
 
@@ -86,22 +82,20 @@ class BrowserHttpClientAdapter implements HttpClientAdapter {
     });
 
     Timer? connectTimeoutTimer;
-
-    final connectionTimeout = options.connectTimeout;
-    if (connectionTimeout != null) {
+    if (connectTimeout > Duration.zero) {
       connectTimeoutTimer = Timer(
-        connectionTimeout,
+        connectTimeout,
         () {
+          connectTimeoutTimer = null;
           if (completer.isCompleted) {
             // connectTimeout is triggered after the fetch has been completed.
             return;
           }
-
           xhr.abort();
           completer.completeError(
             DioException.connectionTimeout(
               requestOptions: options,
-              timeout: connectionTimeout,
+              timeout: connectTimeout,
             ),
             StackTrace.current,
           );
@@ -123,14 +117,12 @@ class BrowserHttpClientAdapter implements HttpClientAdapter {
         });
       }
 
-      final sendTimeout = options.sendTimeout;
-      if (sendTimeout != null) {
+      if (sendTimeout > Duration.zero) {
         final uploadStopwatch = Stopwatch();
         xhr.upload.onProgress.listen((event) {
           if (!uploadStopwatch.isRunning) {
             uploadStopwatch.start();
           }
-
           final duration = uploadStopwatch.elapsed;
           if (duration > sendTimeout) {
             uploadStopwatch.stop();
@@ -155,7 +147,7 @@ class BrowserHttpClientAdapter implements HttpClientAdapter {
         });
       }
     } else {
-      if (options.sendTimeout != null) {
+      if (sendTimeout > Duration.zero) {
         debugLog(
           'sendTimeout cannot be used without a request body to send',
           StackTrace.current,
@@ -176,18 +168,16 @@ class BrowserHttpClientAdapter implements HttpClientAdapter {
         connectTimeoutTimer = null;
       }
 
-      final receiveTimeout = options.receiveTimeout;
-      if (receiveTimeout != null) {
+      if (receiveTimeout > Duration.zero) {
         if (!downloadStopwatch.isRunning) {
           downloadStopwatch.start();
         }
-
         final duration = downloadStopwatch.elapsed;
         if (duration > receiveTimeout) {
           downloadStopwatch.stop();
           completer.completeError(
             DioException.receiveTimeout(
-              timeout: options.receiveTimeout!,
+              timeout: receiveTimeout,
               requestOptions: options,
             ),
             StackTrace.current,
@@ -195,6 +185,7 @@ class BrowserHttpClientAdapter implements HttpClientAdapter {
           xhr.abort();
         }
       }
+
       if (options.onReceiveProgress != null) {
         if (event.loaded != null && event.total != null) {
           options.onReceiveProgress!(event.loaded!, event.total!);
@@ -218,17 +209,27 @@ class BrowserHttpClientAdapter implements HttpClientAdapter {
     });
 
     xhr.onTimeout.first.then((_) {
+      final isConnectTimeout = connectTimeoutTimer != null;
       if (connectTimeoutTimer != null) {
         connectTimeoutTimer?.cancel();
       }
       if (!completer.isCompleted) {
-        completer.completeError(
-          DioException.receiveTimeout(
-            timeout: Duration(milliseconds: xhrTimeout),
-            requestOptions: options,
-          ),
-          StackTrace.current,
-        );
+        if (isConnectTimeout) {
+          completer.completeError(
+            DioException.connectionTimeout(
+              timeout: connectTimeout,
+              requestOptions: options,
+            ),
+          );
+        } else {
+          completer.completeError(
+            DioException.receiveTimeout(
+              timeout: Duration(milliseconds: xhrTimeout),
+              requestOptions: options,
+            ),
+            StackTrace.current,
+          );
+        }
       }
     });
 
diff --git a/dio/lib/src/adapters/io_adapter.dart b/dio/lib/src/adapters/io_adapter.dart
index 65e9ac6cf..35a1b781b 100644
--- a/dio/lib/src/adapters/io_adapter.dart
+++ b/dio/lib/src/adapters/io_adapter.dart
@@ -92,7 +92,7 @@ class IOHttpClientAdapter implements HttpClientAdapter {
     late HttpClientRequest request;
     try {
       final connectionTimeout = options.connectTimeout;
-      if (connectionTimeout != null) {
+      if (connectionTimeout != null && connectionTimeout > Duration.zero) {
         request = await reqFuture.timeout(
           connectionTimeout,
           onTimeout: () {
@@ -116,11 +116,19 @@ class IOHttpClientAdapter implements HttpClientAdapter {
       });
     } on SocketException catch (e) {
       if (e.message.contains('timed out')) {
+        final Duration effectiveTimeout;
+        if (options.connectTimeout != null &&
+            options.connectTimeout! > Duration.zero) {
+          effectiveTimeout = options.connectTimeout!;
+        } else if (httpClient.connectionTimeout != null &&
+            httpClient.connectionTimeout! > Duration.zero) {
+          effectiveTimeout = httpClient.connectionTimeout!;
+        } else {
+          effectiveTimeout = Duration.zero;
+        }
         throw DioException.connectionTimeout(
           requestOptions: options,
-          timeout: options.connectTimeout ??
-              httpClient.connectionTimeout ??
-              Duration.zero,
+          timeout: effectiveTimeout,
           error: e,
         );
       }
@@ -139,7 +147,7 @@ class IOHttpClientAdapter implements HttpClientAdapter {
       // Transform the request data.
       Future<dynamic> future = request.addStream(requestStream);
       final sendTimeout = options.sendTimeout;
-      if (sendTimeout != null) {
+      if (sendTimeout != null && sendTimeout > Duration.zero) {
         future = future.timeout(
           sendTimeout,
           onTimeout: () {
@@ -157,7 +165,7 @@ class IOHttpClientAdapter implements HttpClientAdapter {
     final stopwatch = Stopwatch()..start();
     Future<HttpClientResponse> future = request.close();
     final receiveTimeout = options.receiveTimeout;
-    if (receiveTimeout != null) {
+    if (receiveTimeout != null && receiveTimeout > Duration.zero) {
       future = future.timeout(
         receiveTimeout,
         onTimeout: () {
@@ -195,7 +203,9 @@ class IOHttpClientAdapter implements HttpClientAdapter {
           stopwatch.stop();
           final duration = stopwatch.elapsed;
           final receiveTimeout = options.receiveTimeout;
-          if (receiveTimeout != null && duration > receiveTimeout) {
+          if (receiveTimeout != null &&
+              receiveTimeout > Duration.zero &&
+              duration > receiveTimeout) {
             sink.addError(
               DioException.receiveTimeout(
                 timeout: receiveTimeout,
@@ -228,8 +238,11 @@ class IOHttpClientAdapter implements HttpClientAdapter {
   }
 
   HttpClient _configHttpClient(Duration? connectionTimeout) {
-    return (_cachedHttpClient ??= _createHttpClient())
-      ..connectionTimeout = connectionTimeout;
+    _cachedHttpClient ??= _createHttpClient();
+    if (connectionTimeout != null && connectionTimeout > Duration.zero) {
+      _cachedHttpClient!.connectionTimeout = connectionTimeout;
+    }
+    return _cachedHttpClient!;
   }
 
   @override
diff --git a/dio/lib/src/options.dart b/dio/lib/src/options.dart
index d455afc36..44038d380 100644
--- a/dio/lib/src/options.dart
+++ b/dio/lib/src/options.dart
@@ -107,7 +107,7 @@ mixin OptionsMixin {
   /// [Dio] will throw the [DioException] with
   /// [DioExceptionType.connectionTimeout] type when time out.
   ///
-  /// `null` meanings no timeout limit.
+  /// `null` or `Duration.zero` meanings no timeout limit.
   Duration? get connectTimeout => _connectTimeout;
   Duration? _connectTimeout;
 
@@ -363,7 +363,7 @@ class Options {
   /// [Dio] will throw the [DioException] with
   /// [DioExceptionType.sendTimeout] type when timed out.
   ///
-  /// `null` meanings no timeout limit.
+  /// `null` or `Duration.zero` meanings no timeout limit.
   Duration? get sendTimeout => _sendTimeout;
   Duration? _sendTimeout;
 
@@ -382,7 +382,7 @@ class Options {
   /// [Dio] will throw the [DioException] with
   /// [DioExceptionType.receiveTimeout] type when time out.
   ///
-  /// `null` meanings no timeout limit.
+  /// `null` or `Duration.zero` meanings no timeout limit.
   Duration? get receiveTimeout => _receiveTimeout;
   Duration? _receiveTimeout;
 
diff --git a/plugins/http2_adapter/CHANGELOG.md b/plugins/http2_adapter/CHANGELOG.md
index 8e3ef62ce..d8333501c 100644
--- a/plugins/http2_adapter/CHANGELOG.md
+++ b/plugins/http2_adapter/CHANGELOG.md
@@ -4,6 +4,7 @@
 
 - Implement `sendTimeout` and `receiveTimeout` for the adapter.
 - Fix redirect not working when requestStream is null.
+- Ignores `Duration.zero` timeouts.
 
 ## 2.3.1+1
 
diff --git a/plugins/http2_adapter/lib/src/connection_manager_imp.dart b/plugins/http2_adapter/lib/src/connection_manager_imp.dart
index 1ef0bd5d7..0a8edaa4c 100644
--- a/plugins/http2_adapter/lib/src/connection_manager_imp.dart
+++ b/plugins/http2_adapter/lib/src/connection_manager_imp.dart
@@ -85,7 +85,7 @@ class _ConnectionManager implements ConnectionManager {
       if (e.osError == null) {
         if (e.message.contains('timed out')) {
           throw DioException.connectionTimeout(
-            timeout: options.connectTimeout!,
+            timeout: options.connectTimeout ?? Duration.zero,
             requestOptions: options,
           );
         }
@@ -135,11 +135,16 @@ class _ConnectionManager implements ConnectionManager {
     RequestOptions options,
     ClientSetting clientConfig,
   ) async {
-    if (clientConfig.proxy == null) {
+    final timeout = (options.connectTimeout ?? Duration.zero) > Duration.zero
+        ? options.connectTimeout!
+        : null;
+    final proxy = clientConfig.proxy;
+
+    if (proxy == null) {
       return SecureSocket.connect(
         target.host,
         target.port,
-        timeout: options.connectTimeout,
+        timeout: timeout,
         context: clientConfig.context,
         onBadCertificate: clientConfig.onBadCertificate,
         supportedProtocols: ['h2'],
@@ -147,13 +152,12 @@ class _ConnectionManager implements ConnectionManager {
     }
 
     final proxySocket = await Socket.connect(
-      clientConfig.proxy!.host,
-      clientConfig.proxy!.port,
-      timeout: options.connectTimeout,
+      proxy.host,
+      proxy.port,
+      timeout: timeout,
     );
 
-    final String credentialsProxy =
-        base64Encode(utf8.encode(clientConfig.proxy!.userInfo));
+    final String credentialsProxy = base64Encode(utf8.encode(proxy.userInfo));
 
     // Create http tunnel proxy https://www.ietf.org/rfc/rfc2817.txt
 
diff --git a/plugins/http2_adapter/lib/src/http2_adapter.dart b/plugins/http2_adapter/lib/src/http2_adapter.dart
index e235d0e4a..0c57d62da 100644
--- a/plugins/http2_adapter/lib/src/http2_adapter.dart
+++ b/plugins/http2_adapter/lib/src/http2_adapter.dart
@@ -92,8 +92,8 @@ class Http2Adapter implements HttpClientAdapter {
       final requestStreamFuture = requestStream!.listen((data) {
         stream.outgoingMessages.add(DataStreamMessage(data));
       }).asFuture();
-      final sendTimeout = options.sendTimeout;
-      if (sendTimeout != null) {
+      final sendTimeout = options.sendTimeout ?? Duration.zero;
+      if (sendTimeout > Duration.zero) {
         await requestStreamFuture.timeout(
           sendTimeout,
           onTimeout: () {
@@ -158,8 +158,8 @@ class Http2Adapter implements HttpClientAdapter {
       cancelOnError: true,
     );
 
-    final receiveTimeout = options.receiveTimeout;
-    if (receiveTimeout != null) {
+    final receiveTimeout = options.receiveTimeout ?? Duration.zero;
+    if (receiveTimeout > Duration.zero) {
       await completer.future.timeout(
         receiveTimeout,
         onTimeout: () {


Test patch:
diff --git a/dio/test/timeout_test.dart b/dio/test/timeout_test.dart
index 7af397f78..367b7fd5e 100644
--- a/dio/test/timeout_test.dart
+++ b/dio/test/timeout_test.dart
@@ -70,4 +70,31 @@ void main() {
     } on DioException catch (_) {}
     expect(http.connectionTimeout?.inSeconds == 1, isTrue);
   }, testOn: 'vm');
+
+  test('ignores zero duration timeouts', () async {
+    final dio = Dio(
+      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);
+    // Override with request options.
+    await dio.get(
+      '/drip-lines?delay=1',
+      options: Options(
+        sendTimeout: Duration.zero,
+        receiveTimeout: Duration.zero,
+      ),
+    );
+    await dio.get('/drip-lines?delay=1');
+  });
 }