blob: ce67906cf6b1106bf5dfdeba54a852ffa509d31d [file] [log] [blame]
// Copyright (c) 2021, 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.
// @dart=2.9
import 'dart:async';
import 'dart:io';
import 'dart:math';
/// An HTTP client wrapper that times out connections and requests and
/// automatically retries failing requests.
class RetryTimeoutClient {
/// The wrapped client.
final HttpClient _inner;
/// The number of times a request should be retried.
final int _retries;
/// The callback that determines whether a request should be retried.
final bool Function(HttpClientResponse) _when;
/// The callback that determines whether a request when an error is thrown.
final bool Function(Object, StackTrace) _whenError;
/// The callback that determines how long to wait before retrying a request.
final Duration Function(int) _delay;
/// The callback that determines when to cancel a connection.
final Duration Function(int) _connectionTimeout;
/// The callback that determines when to cancel a request.
final Duration Function(int) _reponseTimeout;
/// The callback to call to indicate that a request is being retried.
final void Function(Uri, HttpClientResponse, int) _onRetry;
/// Creates a client wrapping [_inner] that retries HTTP requests.
this._inner, {
int retries = 3,
bool Function(HttpClientResponse) when = _defaultWhen,
bool Function(Object, StackTrace) whenError = _defaultWhenError,
Duration Function(int retryCount) delay = _defaultDelay,
Duration Function(int retryCount) connectionTimeout = _defaultTimeout,
Duration Function(int retryCount) responseTimeout = _defaultTimeout,
void Function(Uri, HttpClientResponse, int retryCount) onRetry,
}) : _retries = retries,
_when = when,
_whenError = whenError,
_delay = delay,
_connectionTimeout = connectionTimeout,
_reponseTimeout = responseTimeout,
_onRetry = onRetry {
RangeError.checkNotNegative(_retries, 'retries');
Future<HttpClientResponse> headUrl(Uri url) {
return _retry(url, _inner.headUrl);
Future<HttpClientResponse> getUrl(Uri url) {
return _retry(url, _inner.getUrl);
Future<HttpClientResponse> _retry(
Uri url, Future<HttpClientRequest> Function(Uri) method) async {
var i = 0;
for (;;) {
HttpClientResponse response;
try {
_inner.connectionTimeout = _connectionTimeout(i);
var request = await method(url).timeout(
onTimeout: () =>
throw TimeoutException('$url, retry:$i', _reponseTimeout(i)),
response = await request.close();
} catch (error, stackTrace) {
if (i == _retries || !_whenError(error, stackTrace)) rethrow;
if (response != null) {
if (i == _retries || !_when(response)) return response;
// Make sure the response stream is listened to so that we don't leave
// dangling connections.
unawaited(response.listen((_) {}).cancel().catchError((_) {}));
await Future.delayed(_delay(i));
_onRetry?.call(url, response, i);
void close({bool force = false}) => _inner.close(force: force);
bool _defaultWhen(HttpClientResponse response) =>
response.statusCode == 500 || response.statusCode == 503;
bool _defaultWhenError(Object error, StackTrace stackTrace) =>
error is OSError ||
error is HttpException ||
error is SocketException ||
error is TimeoutException;
Duration _defaultDelay(int retryCount) =>
const Duration(milliseconds: 500) * pow(1.5, retryCount);
Duration _defaultTimeout(int retryCount) =>
const Duration(milliseconds: 5000) * pow(1.5, retryCount);