Skip to content

Commit

Permalink
Add a feature allowing to specify extra retryable status codes (param…
Browse files Browse the repository at this point in the history
…eter `retryableExtraStatuses`) (#11)
  • Loading branch information
rodion-m committed Jul 17, 2022
1 parent 9bd641b commit 22dd670
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 44 deletions.
8 changes: 5 additions & 3 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion CHANGELOG.md
@@ -1,6 +1,10 @@
## 1.3.0
- Add a feature allowing to specify extra retryable status codes (parameter `retryableExtraStatuses`) (#11)
- Update dependencies

## 1.2.0

- Add properly incorrect url scheme error handling in default evaluator (#2)
- Add properly an incorrect url scheme error handling in the default retry evaluator (#2)

## 1.1.0

Expand Down
18 changes: 17 additions & 1 deletion README.md
Expand Up @@ -11,7 +11,7 @@ Also, it supports dynamic delay between retries. \

## Getting started

1. Add package to pubspec.yaml: `dio_smart_retry: ^1.2.0`
1. Add package to pubspec.yaml: `dio_smart_retry: ^1.3.0`
2. Import package: `import 'package:dio_smart_retry/dio_smart_retry.dart'`

## Usage
Expand Down Expand Up @@ -70,3 +70,19 @@ final options = Options()
..disableRetry = true;
await dio.get<String>('/', options: options);
```

## Add extra retryable status codes
It's possible to add you own retryable status codes. Use `retryableExtraStatuses` option for that. Here is an example:
```dart
RetryInterceptor(
dio: dio,
retryableExtraStatuses: { status401Unauthorized },
)
```
or:
```dart
RetryInterceptor(
dio: dio,
retryableExtraStatuses: { 401 },
)
```
15 changes: 9 additions & 6 deletions example/dio_smart_retry_example.dart
@@ -1,24 +1,27 @@
import 'package:dio/dio.dart';
import 'package:dio_smart_retry/dio_smart_retry.dart';
import 'package:dio_smart_retry/src/http_status_codes.dart';

Future main() async {
Future<dynamic> main() async {
final dio = Dio();

// Add the interceptor
dio.interceptors.add(
RetryInterceptor(
dio: dio,
logPrint: print, // specify log function (optional)
retries: 3, // retry count (optional)
retries: 4, // retry count (optional)
retryDelays: const [
// set delays between retries (optional)
Duration(seconds: 1), // wait 1 sec before first retry
Duration(seconds: 2), // wait 2 sec before second retry
Duration(seconds: 3), // wait 3 sec before third retry
Duration(seconds: 1), // wait 1 sec before the first retry
Duration(seconds: 2), // wait 2 sec before the second retry
Duration(seconds: 3), // wait 3 sec before the third retry
Duration(seconds: 4), // wait 4 sec before the fourth retry
],
retryableExtraStatuses: { status401Unauthorized },
),
);

/// Sending a failing request for 3 times with 1s, then 2s, then 3s interval
await dio.get<dynamic>('https://mock.codes/500');
await dio.get<dynamic>('https://mock.codes/401');
}
30 changes: 30 additions & 0 deletions lib/src/default_retry_evaluator.dart
@@ -0,0 +1,30 @@
import 'dart:async';

import 'package:dio/dio.dart';

class DefaultRetryEvaluator {
DefaultRetryEvaluator(this._retryableStatuses);

final Set<int> _retryableStatuses;

/// Returns true only if the response hasn't been cancelled
/// or got a bad status code.
// ignore: avoid-unused-parameters
FutureOr<bool> evaluate(DioError error, int attempt) {
bool shouldRetry;
if (error.type == DioErrorType.response) {
final statusCode = error.response?.statusCode;
if (statusCode != null) {
shouldRetry = isRetryable(statusCode);
} else {
shouldRetry = true;
}
} else {
shouldRetry =
error.type != DioErrorType.cancel && error.error is! FormatException;
}
return shouldRetry;
}

bool isRetryable(int statusCode) => _retryableStatuses.contains(statusCode);
}
14 changes: 12 additions & 2 deletions lib/src/http_status_codes.dart
@@ -1,3 +1,5 @@
import 'package:dio_smart_retry/dio_smart_retry.dart';

const status100Continue = 100;
const status101SwitchingProtocols = 101;
const status102Processing = 102;
Expand Down Expand Up @@ -91,7 +93,7 @@ const status499ClientClosedRequest = 499;
/// From AWS Elastic Load Balancer
const status460ClientClosedRequest = 460;

const retryableStatuses = <int>{
const defaultRetryableStatuses = <int>{
status408RequestTimeout,
status429TooManyRequests,
status500InternalServerError,
Expand All @@ -112,4 +114,12 @@ const retryableStatuses = <int>{
status527RailgunError,
};

bool isRetryable(int statusCode) => retryableStatuses.contains(statusCode);
// For backward compatibility purpose
@Deprecated('Use [defaultRetryableStatuses]')
const retryableStatuses = defaultRetryableStatuses;

// For backward compatibility purpose
/// Be careful: this method do not
/// take into account [RetryInterceptor.retryableExtraStatuses]
bool isRetryable(int statusCode)
=> defaultRetryableStatuses.contains(statusCode);
59 changes: 33 additions & 26 deletions lib/src/retry_interceptor.dart
Expand Up @@ -2,7 +2,8 @@ import 'dart:async';

import 'package:dio/dio.dart';

import 'http_status_codes.dart';
import 'package:dio_smart_retry/src/default_retry_evaluator.dart';
import 'package:dio_smart_retry/src/http_status_codes.dart';

typedef RetryEvaluator = FutureOr<bool> Function(DioError error, int attempt);

Expand All @@ -19,13 +20,26 @@ class RetryInterceptor extends Interceptor {
],
RetryEvaluator? retryEvaluator,
this.ignoreRetryEvaluatorExceptions = false,
}) : _retryEvaluator = retryEvaluator ?? defaultRetryEvaluator;
this.retryableExtraStatuses = const {},
}) : _retryEvaluator = retryEvaluator ?? DefaultRetryEvaluator({
...defaultRetryableStatuses,
...retryableExtraStatuses,
}).evaluate {
if(retryEvaluator != null && retryableExtraStatuses.isNotEmpty) {
throw ArgumentError(
'[retryableExtraStatuses] works only if [retryEvaluator] is null.'
' Set either [retryableExtraStatuses] or [retryEvaluator].'
' Not both.',
'retryableExtraStatuses',
);
}
}

/// The original dio
final Dio dio;

/// For logging purpose
final Function(String message)? logPrint;
final void Function(String message)? logPrint;

/// The number of retry in case of an error
final int retries;
Expand All @@ -37,36 +51,29 @@ class RetryInterceptor extends Interceptor {
/// Empty [retryDelays] means no delay.
///
/// If [retries] count more than [retryDelays] count,
/// the last element value of [retryDelays] will be used.
/// the last element value of [retryDelays] will be used.
final List<Duration> retryDelays;

/// Evaluating if a retry is necessary.regarding the error.
///
/// It can be a good candidate for additional operations too, like
/// updating authentication token in case of a unauthorized error
/// (be careful with concurrency though).
/// updating authentication token in case of a unauthorized error
/// (be careful with concurrency though).
///
/// Defaults to [defaultRetryEvaluator].
/// Defaults to [DefaultRetryEvaluator.evaluate]
/// with [defaultRetryableStatuses].
final RetryEvaluator _retryEvaluator;

/// Returns true only if the response hasn't been cancelled or got
/// a bad status code.
// ignore: avoid-unused-parameters
static FutureOr<bool> defaultRetryEvaluator(DioError error, int attempt) {
bool shouldRetry;
if (error.type == DioErrorType.response) {
final statusCode = error.response?.statusCode;
if (statusCode != null) {
shouldRetry = isRetryable(statusCode);
} else {
shouldRetry = true;
}
} else {
shouldRetry = error.type != DioErrorType.cancel
&& error.error is! FormatException;
}
return shouldRetry;
}
/// Specifies an extra retryable statuses,
/// which will be taken into account with [defaultRetryableStatuses]
/// IMPORTANT: THIS SETTING WORKS ONLY IF [_retryEvaluator] is null
final Set<int> retryableExtraStatuses;

/// Redirects to [DefaultRetryEvaluator.evaluate]
/// with [defaultRetryableStatuses]
static final FutureOr<bool> Function(DioError error, int attempt)
defaultRetryEvaluator =
DefaultRetryEvaluator(defaultRetryableStatuses).evaluate;

Future<bool> _shouldRetry(DioError error, int attempt) async {
try {
Expand All @@ -81,7 +88,7 @@ class RetryInterceptor extends Interceptor {
}

@override
Future onError(DioError err, ErrorInterceptorHandler handler) async {
Future<dynamic> onError(DioError err, ErrorInterceptorHandler handler) async {
if (err.requestOptions.disableRetry) {
return super.onError(err, handler);
}
Expand Down
10 changes: 5 additions & 5 deletions pubspec.yaml
@@ -1,6 +1,6 @@
name: dio_smart_retry
description: Retry library for Dio package made with love. By default, the request will be retried only for appropriate retryable http statuses.
version: 1.2.0
version: 1.3.0
repository: https://github.com/rodion-m/dio_smart_retry
issue_tracker: https://github.com/rodion-m/dio_smart_retry/issues
homepage: https://github.com/rodion-m/dio_smart_retry
Expand All @@ -10,9 +10,9 @@ environment:
sdk: '>=2.12.0 <3.0.0'

dependencies:
dio: ^4.0.4
dio: ^4.0.6

dev_dependencies:
test: ^1.21.1
very_good_analysis: ^2.4.0
dart_code_metrics: ^4.10.1
dart_code_metrics: ^4.16.0
test: ^1.21.4
very_good_analysis: ^3.0.1

0 comments on commit 22dd670

Please sign in to comment.