Skip to content

Commit 3595442

Browse files
authored
⚡ Ignore zero duration timeout (#2029)
Resolves #2022. ### New Pull Request Checklist - [x] I have read the [Documentation](https://pub.dev/documentation/dio/latest/) - [x] I have searched for a similar pull request in the [project](https://github.com/cfug/dio/pulls) and found none - [x] I have updated this branch with the latest `main` branch to avoid conflicts (via merge from master or rebase) - [x] I have added the required tests to prove the fix/feature I'm adding - [x] I have updated the documentation (if necessary) - [x] I have run the tests without failures - [x] I have updated the `CHANGELOG.md` in the corresponding package ### Additional context and info (if any) I've found that the `IOHttpClientAdapter` uses `Stopwatch` to integrate with `receiveTimeout`, which might cause infinity awaits if the stream has no response forever. This might be the root cause of #1739. https://github.com/cfug/dio/blob/78f3813a8d8948887198ef628e5a2e2039489b03/dio/lib/src/adapters/io_adapter.dart#L194-L200 --------- Signed-off-by: Alex Li <github@alexv525.com>
1 parent bedcc54 commit 3595442

File tree

8 files changed

+103
-56
lines changed

8 files changed

+103
-56
lines changed

dio/CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ See the [Migration Guide][] for the complete breaking changes list.**
66
## Unreleased
77

88
- Raise warning for `Map`s other than `Map<String, dynamic>` when encoding request data.
9-
- Improve exception messages
10-
- Allow `ResponseDecoder` and `RequestEncoder` to be async
9+
- Improve exception messages.
10+
- Allow `ResponseDecoder` and `RequestEncoder` to be async.
11+
- Ignores `Duration.zero` timeouts.
1112

1213
## 5.3.3
1314

dio/lib/src/adapters/browser_adapter.dart

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,11 @@ class BrowserHttpClientAdapter implements HttpClientAdapter {
5858
}
5959
});
6060

61-
final connectTimeout = options.connectTimeout;
62-
final receiveTimeout = options.receiveTimeout;
63-
int xhrTimeout = 0;
64-
if (connectTimeout != null &&
65-
receiveTimeout != null &&
66-
receiveTimeout > Duration.zero) {
67-
xhrTimeout = (connectTimeout + receiveTimeout).inMilliseconds;
68-
xhr.timeout = xhrTimeout;
69-
}
61+
final sendTimeout = options.sendTimeout ?? Duration.zero;
62+
final connectTimeout = options.connectTimeout ?? Duration.zero;
63+
final receiveTimeout = options.receiveTimeout ?? Duration.zero;
64+
final xhrTimeout = (connectTimeout + receiveTimeout).inMilliseconds;
65+
xhr.timeout = xhrTimeout;
7066

7167
final completer = Completer<ResponseBody>();
7268

@@ -86,22 +82,20 @@ class BrowserHttpClientAdapter implements HttpClientAdapter {
8682
});
8783

8884
Timer? connectTimeoutTimer;
89-
90-
final connectionTimeout = options.connectTimeout;
91-
if (connectionTimeout != null) {
85+
if (connectTimeout > Duration.zero) {
9286
connectTimeoutTimer = Timer(
93-
connectionTimeout,
87+
connectTimeout,
9488
() {
89+
connectTimeoutTimer = null;
9590
if (completer.isCompleted) {
9691
// connectTimeout is triggered after the fetch has been completed.
9792
return;
9893
}
99-
10094
xhr.abort();
10195
completer.completeError(
10296
DioException.connectionTimeout(
10397
requestOptions: options,
104-
timeout: connectionTimeout,
98+
timeout: connectTimeout,
10599
),
106100
StackTrace.current,
107101
);
@@ -123,14 +117,12 @@ class BrowserHttpClientAdapter implements HttpClientAdapter {
123117
});
124118
}
125119

126-
final sendTimeout = options.sendTimeout;
127-
if (sendTimeout != null) {
120+
if (sendTimeout > Duration.zero) {
128121
final uploadStopwatch = Stopwatch();
129122
xhr.upload.onProgress.listen((event) {
130123
if (!uploadStopwatch.isRunning) {
131124
uploadStopwatch.start();
132125
}
133-
134126
final duration = uploadStopwatch.elapsed;
135127
if (duration > sendTimeout) {
136128
uploadStopwatch.stop();
@@ -155,7 +147,7 @@ class BrowserHttpClientAdapter implements HttpClientAdapter {
155147
});
156148
}
157149
} else {
158-
if (options.sendTimeout != null) {
150+
if (sendTimeout > Duration.zero) {
159151
debugLog(
160152
'sendTimeout cannot be used without a request body to send',
161153
StackTrace.current,
@@ -176,25 +168,24 @@ class BrowserHttpClientAdapter implements HttpClientAdapter {
176168
connectTimeoutTimer = null;
177169
}
178170

179-
final receiveTimeout = options.receiveTimeout;
180-
if (receiveTimeout != null) {
171+
if (receiveTimeout > Duration.zero) {
181172
if (!downloadStopwatch.isRunning) {
182173
downloadStopwatch.start();
183174
}
184-
185175
final duration = downloadStopwatch.elapsed;
186176
if (duration > receiveTimeout) {
187177
downloadStopwatch.stop();
188178
completer.completeError(
189179
DioException.receiveTimeout(
190-
timeout: options.receiveTimeout!,
180+
timeout: receiveTimeout,
191181
requestOptions: options,
192182
),
193183
StackTrace.current,
194184
);
195185
xhr.abort();
196186
}
197187
}
188+
198189
if (options.onReceiveProgress != null) {
199190
if (event.loaded != null && event.total != null) {
200191
options.onReceiveProgress!(event.loaded!, event.total!);
@@ -218,17 +209,27 @@ class BrowserHttpClientAdapter implements HttpClientAdapter {
218209
});
219210

220211
xhr.onTimeout.first.then((_) {
212+
final isConnectTimeout = connectTimeoutTimer != null;
221213
if (connectTimeoutTimer != null) {
222214
connectTimeoutTimer?.cancel();
223215
}
224216
if (!completer.isCompleted) {
225-
completer.completeError(
226-
DioException.receiveTimeout(
227-
timeout: Duration(milliseconds: xhrTimeout),
228-
requestOptions: options,
229-
),
230-
StackTrace.current,
231-
);
217+
if (isConnectTimeout) {
218+
completer.completeError(
219+
DioException.connectionTimeout(
220+
timeout: connectTimeout,
221+
requestOptions: options,
222+
),
223+
);
224+
} else {
225+
completer.completeError(
226+
DioException.receiveTimeout(
227+
timeout: Duration(milliseconds: xhrTimeout),
228+
requestOptions: options,
229+
),
230+
StackTrace.current,
231+
);
232+
}
232233
}
233234
});
234235

dio/lib/src/adapters/io_adapter.dart

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ class IOHttpClientAdapter implements HttpClientAdapter {
9292
late HttpClientRequest request;
9393
try {
9494
final connectionTimeout = options.connectTimeout;
95-
if (connectionTimeout != null) {
95+
if (connectionTimeout != null && connectionTimeout > Duration.zero) {
9696
request = await reqFuture.timeout(
9797
connectionTimeout,
9898
onTimeout: () {
@@ -116,11 +116,19 @@ class IOHttpClientAdapter implements HttpClientAdapter {
116116
});
117117
} on SocketException catch (e) {
118118
if (e.message.contains('timed out')) {
119+
final Duration effectiveTimeout;
120+
if (options.connectTimeout != null &&
121+
options.connectTimeout! > Duration.zero) {
122+
effectiveTimeout = options.connectTimeout!;
123+
} else if (httpClient.connectionTimeout != null &&
124+
httpClient.connectionTimeout! > Duration.zero) {
125+
effectiveTimeout = httpClient.connectionTimeout!;
126+
} else {
127+
effectiveTimeout = Duration.zero;
128+
}
119129
throw DioException.connectionTimeout(
120130
requestOptions: options,
121-
timeout: options.connectTimeout ??
122-
httpClient.connectionTimeout ??
123-
Duration.zero,
131+
timeout: effectiveTimeout,
124132
error: e,
125133
);
126134
}
@@ -139,7 +147,7 @@ class IOHttpClientAdapter implements HttpClientAdapter {
139147
// Transform the request data.
140148
Future<dynamic> future = request.addStream(requestStream);
141149
final sendTimeout = options.sendTimeout;
142-
if (sendTimeout != null) {
150+
if (sendTimeout != null && sendTimeout > Duration.zero) {
143151
future = future.timeout(
144152
sendTimeout,
145153
onTimeout: () {
@@ -157,7 +165,7 @@ class IOHttpClientAdapter implements HttpClientAdapter {
157165
final stopwatch = Stopwatch()..start();
158166
Future<HttpClientResponse> future = request.close();
159167
final receiveTimeout = options.receiveTimeout;
160-
if (receiveTimeout != null) {
168+
if (receiveTimeout != null && receiveTimeout > Duration.zero) {
161169
future = future.timeout(
162170
receiveTimeout,
163171
onTimeout: () {
@@ -195,7 +203,9 @@ class IOHttpClientAdapter implements HttpClientAdapter {
195203
stopwatch.stop();
196204
final duration = stopwatch.elapsed;
197205
final receiveTimeout = options.receiveTimeout;
198-
if (receiveTimeout != null && duration > receiveTimeout) {
206+
if (receiveTimeout != null &&
207+
receiveTimeout > Duration.zero &&
208+
duration > receiveTimeout) {
199209
sink.addError(
200210
DioException.receiveTimeout(
201211
timeout: receiveTimeout,
@@ -228,8 +238,11 @@ class IOHttpClientAdapter implements HttpClientAdapter {
228238
}
229239

230240
HttpClient _configHttpClient(Duration? connectionTimeout) {
231-
return (_cachedHttpClient ??= _createHttpClient())
232-
..connectionTimeout = connectionTimeout;
241+
_cachedHttpClient ??= _createHttpClient();
242+
if (connectionTimeout != null && connectionTimeout > Duration.zero) {
243+
_cachedHttpClient!.connectionTimeout = connectionTimeout;
244+
}
245+
return _cachedHttpClient!;
233246
}
234247

235248
@override

dio/lib/src/options.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ mixin OptionsMixin {
107107
/// [Dio] will throw the [DioException] with
108108
/// [DioExceptionType.connectionTimeout] type when time out.
109109
///
110-
/// `null` meanings no timeout limit.
110+
/// `null` or `Duration.zero` meanings no timeout limit.
111111
Duration? get connectTimeout => _connectTimeout;
112112
Duration? _connectTimeout;
113113

@@ -363,7 +363,7 @@ class Options {
363363
/// [Dio] will throw the [DioException] with
364364
/// [DioExceptionType.sendTimeout] type when timed out.
365365
///
366-
/// `null` meanings no timeout limit.
366+
/// `null` or `Duration.zero` meanings no timeout limit.
367367
Duration? get sendTimeout => _sendTimeout;
368368
Duration? _sendTimeout;
369369

@@ -382,7 +382,7 @@ class Options {
382382
/// [Dio] will throw the [DioException] with
383383
/// [DioExceptionType.receiveTimeout] type when time out.
384384
///
385-
/// `null` meanings no timeout limit.
385+
/// `null` or `Duration.zero` meanings no timeout limit.
386386
Duration? get receiveTimeout => _receiveTimeout;
387387
Duration? _receiveTimeout;
388388

dio/test/timeout_test.dart

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,31 @@ void main() {
7070
} on DioException catch (_) {}
7171
expect(http.connectionTimeout?.inSeconds == 1, isTrue);
7272
}, testOn: 'vm');
73+
74+
test('ignores zero duration timeouts', () async {
75+
final dio = Dio(
76+
BaseOptions(
77+
baseUrl: 'https://httpbun.com/',
78+
connectTimeout: Duration.zero,
79+
sendTimeout: Duration.zero,
80+
receiveTimeout: Duration.zero,
81+
),
82+
);
83+
// Ignores zero duration timeouts from the base options.
84+
await dio.get('/drip-lines?delay=1');
85+
// Reset the base options.
86+
dio.options
87+
..connectTimeout = Duration(seconds: 10)
88+
..sendTimeout = Duration(seconds: 10)
89+
..receiveTimeout = Duration(seconds: 10);
90+
// Override with request options.
91+
await dio.get(
92+
'/drip-lines?delay=1',
93+
options: Options(
94+
sendTimeout: Duration.zero,
95+
receiveTimeout: Duration.zero,
96+
),
97+
);
98+
await dio.get('/drip-lines?delay=1');
99+
});
73100
}

plugins/http2_adapter/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- Implement `sendTimeout` and `receiveTimeout` for the adapter.
66
- Fix redirect not working when requestStream is null.
7+
- Ignores `Duration.zero` timeouts.
78

89
## 2.3.1+1
910

plugins/http2_adapter/lib/src/connection_manager_imp.dart

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ class _ConnectionManager implements ConnectionManager {
8585
if (e.osError == null) {
8686
if (e.message.contains('timed out')) {
8787
throw DioException.connectionTimeout(
88-
timeout: options.connectTimeout!,
88+
timeout: options.connectTimeout ?? Duration.zero,
8989
requestOptions: options,
9090
);
9191
}
@@ -135,25 +135,29 @@ class _ConnectionManager implements ConnectionManager {
135135
RequestOptions options,
136136
ClientSetting clientConfig,
137137
) async {
138-
if (clientConfig.proxy == null) {
138+
final timeout = (options.connectTimeout ?? Duration.zero) > Duration.zero
139+
? options.connectTimeout!
140+
: null;
141+
final proxy = clientConfig.proxy;
142+
143+
if (proxy == null) {
139144
return SecureSocket.connect(
140145
target.host,
141146
target.port,
142-
timeout: options.connectTimeout,
147+
timeout: timeout,
143148
context: clientConfig.context,
144149
onBadCertificate: clientConfig.onBadCertificate,
145150
supportedProtocols: ['h2'],
146151
);
147152
}
148153

149154
final proxySocket = await Socket.connect(
150-
clientConfig.proxy!.host,
151-
clientConfig.proxy!.port,
152-
timeout: options.connectTimeout,
155+
proxy.host,
156+
proxy.port,
157+
timeout: timeout,
153158
);
154159

155-
final String credentialsProxy =
156-
base64Encode(utf8.encode(clientConfig.proxy!.userInfo));
160+
final String credentialsProxy = base64Encode(utf8.encode(proxy.userInfo));
157161

158162
// Create http tunnel proxy https://www.ietf.org/rfc/rfc2817.txt
159163

plugins/http2_adapter/lib/src/http2_adapter.dart

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ class Http2Adapter implements HttpClientAdapter {
9292
final requestStreamFuture = requestStream!.listen((data) {
9393
stream.outgoingMessages.add(DataStreamMessage(data));
9494
}).asFuture();
95-
final sendTimeout = options.sendTimeout;
96-
if (sendTimeout != null) {
95+
final sendTimeout = options.sendTimeout ?? Duration.zero;
96+
if (sendTimeout > Duration.zero) {
9797
await requestStreamFuture.timeout(
9898
sendTimeout,
9999
onTimeout: () {
@@ -158,8 +158,8 @@ class Http2Adapter implements HttpClientAdapter {
158158
cancelOnError: true,
159159
);
160160

161-
final receiveTimeout = options.receiveTimeout;
162-
if (receiveTimeout != null) {
161+
final receiveTimeout = options.receiveTimeout ?? Duration.zero;
162+
if (receiveTimeout > Duration.zero) {
163163
await completer.future.timeout(
164164
receiveTimeout,
165165
onTimeout: () {

0 commit comments

Comments
 (0)