Skip to content

Instantly share code, notes, and snippets.

@slightfoot
Last active August 13, 2021 12:52
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save slightfoot/fea35818d405556fe7d2d1325d90896d to your computer and use it in GitHub Desktop.
Save slightfoot/fea35818d405556fe7d2d1325d90896d to your computer and use it in GitHub Desktop.
Function to perform a http request with retry and back-off logic. This is modified version from NetworkImageWithRetry - by Simon Lightfoot 13/05/2021
// Copyright 2017, the Flutter 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.
//
// This is modified version from NetworkImageWithRetry - by Simon Lightfoot 13/05/2021
//
// Built from : https://github.com/flutter/flutter_image/blob/master/lib/network.dart
//
import 'dart:async';
import 'dart:convert' show ByteConversionSink;
import 'dart:io' as io;
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:http/http.dart' as http;
import 'package:meta/meta.dart';
///
/// If [fetchStrategy] is specified, uses it instead of the
/// [defaultFetchStrategy] to obtain instructions for fetching the URL.
/// The strategy used to fetch the [url] and retry when the fetch fails.
///
/// This function is called at least once and may be called multiple times.
/// The first time it is called, it is passed a null [FetchFailure], which
/// indicates that this is the first attempt to fetch the [url]. Subsequent
/// calls pass non-null [FetchFailure] values, which indicate that previous
/// fetch attempts failed.
Future<http.Response> performWithRetry({
required http.Client client,
required http.Request request,
FetchStrategy fetchStrategy = defaultFetchStrategy,
}) async {
final stopwatch = Stopwatch()..start();
var instructions = await fetchStrategy(request, null);
_debugCheckInstructions(fetchStrategy, instructions);
var attemptCount = 0;
FetchFailure? lastFailure;
http.StreamedResponse? response;
while (!instructions.shouldGiveUp) {
attemptCount += 1;
try {
response = await client.send(instructions.request).timeout(instructions.timeout);
if (response.statusCode != 200) {
throw FetchFailure._(
totalDuration: stopwatch.elapsed,
attemptCount: attemptCount,
httpStatusCode: response.statusCode,
);
}
final bodyCompleter = Completer<Uint8List>();
final bodySink = ByteConversionSink.withCallback((List<int> accumulated) {
bodyCompleter.complete(Uint8List.fromList(accumulated));
});
response.stream //
.timeout(instructions.timeout)
.listen(
bodySink.add,
onError: bodyCompleter.completeError,
onDone: bodySink.close,
cancelOnError: true,
);
final bytes = await bodyCompleter.future;
if (bytes.lengthInBytes == 0) {
throw FetchFailure._(
totalDuration: stopwatch.elapsed,
attemptCount: attemptCount,
httpStatusCode: response.statusCode,
);
}
return http.Response.bytes(
bytes,
response.statusCode,
request: response.request,
headers: response.headers,
isRedirect: response.isRedirect,
persistentConnection: response.persistentConnection,
reasonPhrase: response.reasonPhrase,
);
} catch (error) {
await response?.stream.drain();
lastFailure = error is FetchFailure
? error
: FetchFailure._(
totalDuration: stopwatch.elapsed,
attemptCount: attemptCount,
originalException: error,
);
instructions = await fetchStrategy(instructions.request, lastFailure);
_debugCheckInstructions(fetchStrategy, instructions);
}
}
assert(lastFailure != null);
throw lastFailure!;
}
void _debugCheckInstructions(FetchStrategy fetchStrategy, FetchInstructions? instructions) {
assert(() {
if (instructions == null) {
if (fetchStrategy == defaultFetchStrategy) {
throw StateError('The default FetchStrategy returned null FetchInstructions.');
} else {
throw StateError('The custom FetchStrategy used returned null\n'
'FetchInstructions. FetchInstructions must never be null, but\n'
'instead instruct to either make another fetch attempt or give up.');
}
}
return true;
}());
}
/// The [FetchStrategy] that [performWithRetry] uses by default.
Future<FetchInstructions> defaultFetchStrategy(http.Request request, FetchFailure? failure) {
return _defaultFetchStrategyFunction(request, failure);
}
/// Used by [defaultFetchStrategy].
///
/// This indirection is necessary because [defaultFetchStrategy] is used as
/// the default constructor argument value, which requires that it be a const
/// expression.
final FetchStrategy _defaultFetchStrategyFunction = const FetchStrategyBuilder().build();
/// This function is called to get [FetchInstructions] to perform the request.
///
/// The instructions are executed as soon as possible after the returned
/// [Future] resolves. If a delay in necessary between retries, use a delayed
/// [Future], such as [Future.delayed]. This is useful for implementing
/// back-off strategies and for recovering from lack of connectivity.
///
/// [request] is the last request used. A [FetchStrategy] may choose to use
/// a different URI (see [FetchInstructions.uri]).
///
/// If [failure] is `null`, then this is the first attempt.
///
/// If the [failure] is not `null`, it contains the information about the
/// previous attempt. A [FetchStrategy] may attempt to recover from the
/// failure by returning [FetchInstructions] that instruct [performWithRetry]
/// to try again.
///
/// See [defaultFetchStrategy] for an example.
typedef FetchStrategy = Future<FetchInstructions> Function(
http.Request request, FetchFailure? failure);
/// Instructions [performWithRetry] uses to perform the request
@immutable
class FetchInstructions {
/// Instructs [performWithRetry] to give up trying to perfomr the request.
const FetchInstructions.giveUp({required this.request})
: shouldGiveUp = true,
timeout = Duration.zero;
/// Instructs [performWithRetry] to attempt to perform the request with the
/// given [request] and [timeout] if it takes too long.
const FetchInstructions.attempt({
required this.request,
required this.timeout,
}) : shouldGiveUp = false;
/// Instructs to give up trying.
///
/// Reports the latest [FetchFailure].
final bool shouldGiveUp;
/// Timeout for the next network call.
final Duration timeout;
/// The URI to use on the next attempt.
final http.Request request;
@override
String toString() {
return '$runtimeType(\n'
' shouldGiveUp: $shouldGiveUp\n'
' timeout: $timeout\n'
' request: $request\n'
')';
}
}
/// Contains information about a failed attempt to perform the request.
@immutable
class FetchFailure implements Exception {
const FetchFailure._({
required this.totalDuration,
required this.attemptCount,
this.httpStatusCode,
this.originalException,
}) : assert(attemptCount > 0);
/// The total amount of time it has taken so far to perform the request.
final Duration totalDuration;
/// The number of times attempted to perform the request so far.
///
/// This value starts with 1 and grows by 1 with each attempt to perform the request.
final int attemptCount;
/// HTTP status code, such as 500.
final int? httpStatusCode;
/// The exception that caused the fetch failure.
final dynamic originalException;
@override
String toString() {
return '$runtimeType(\n'
' attemptCount: $attemptCount\n'
' httpStatusCode: $httpStatusCode\n'
' totalDuration: $totalDuration\n'
' originalException: $originalException\n'
')';
}
}
/// Determines whether the given HTTP [statusCode] is transient.
typedef TransientHttpStatusCodePredicate = bool Function(int statusCode);
/// Builds a [FetchStrategy] function that retries up to a certain amount of
/// times for up to a certain amount of time.
///
/// Pauses between retries with pauses growing exponentially (known as
/// exponential backoff). Each attempt is subject to a [timeout]. Retries only
/// those HTTP status codes considered transient by a
/// [transientHttpStatusCodePredicate] function.
class FetchStrategyBuilder {
/// Creates a fetch strategy builder.
///
/// All parameters must be non-null.
const FetchStrategyBuilder({
this.timeout = const Duration(seconds: 30),
this.totalFetchTimeout = const Duration(minutes: 1),
this.maxAttempts = 5,
this.initialPauseBetweenRetries = const Duration(seconds: 1),
this.exponentialBackoffMultiplier = 2,
this.transientHttpStatusCodePredicate = defaultTransientHttpStatusCodePredicate,
});
/// A list of HTTP status codes that can generally be retried.
///
/// You may want to use a different list depending on the needs of your
/// application.
static const List<int> defaultTransientHttpStatusCodes = <int>[
0, // Network error
408, // Request timeout
500, // Internal server error
502, // Bad gateway
503, // Service unavailable
504 // Gateway timeout
];
/// Maximum amount of time a single fetch attempt is allowed to take.
final Duration timeout;
/// A strategy built by this builder will retry for up to this amount of time
/// before giving up.
final Duration totalFetchTimeout;
/// Maximum number of attempts a strategy will make before giving up.
final int maxAttempts;
/// Initial amount of time between retries.
final Duration initialPauseBetweenRetries;
/// The pause between retries is multiplied by this number with each attempt,
/// causing it to grow exponentially.
final num exponentialBackoffMultiplier;
/// A function that determines whether a given HTTP status code should be
/// retried.
final TransientHttpStatusCodePredicate transientHttpStatusCodePredicate;
/// Uses [defaultTransientHttpStatusCodes] to determine if the [statusCode] is
/// transient.
static bool defaultTransientHttpStatusCodePredicate(int statusCode) {
return defaultTransientHttpStatusCodes.contains(statusCode);
}
/// Builds a [FetchStrategy] that operates using the properties of this
/// builder.
FetchStrategy build() {
return (http.Request request, FetchFailure? failure) async {
if (failure == null) {
// First attempt. Just load.
return FetchInstructions.attempt(request: request, timeout: timeout);
}
final isRetryableFailure = (failure.httpStatusCode != null &&
transientHttpStatusCodePredicate(failure.httpStatusCode!)) ||
failure.originalException is io.SocketException;
// If cannot retry, give up.
if (!isRetryableFailure || // retrying will not help
failure.totalDuration > totalFetchTimeout || // taking too long
failure.attemptCount > maxAttempts) {
// too many attempts
return FetchInstructions.giveUp(request: request);
}
// Exponential back-off.
final pauseBetweenRetries = initialPauseBetweenRetries *
math.pow(exponentialBackoffMultiplier, failure.attemptCount - 1);
await Future<void>.delayed(pauseBetweenRetries);
// Retry.
return FetchInstructions.attempt(request: request, timeout: timeout);
};
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment