Skip to content

Commit 29a8e19

Browse files
committed
feat(interceptor): add RetryInterceptor for handling request retries
1 parent 7ef88af commit 29a8e19

File tree

1 file changed

+155
-0
lines changed

1 file changed

+155
-0
lines changed
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// lib/src/core/retry_interceptor.dart
2+
3+
import 'dart:async';
4+
import 'package:dio/dio.dart';
5+
import 'package:mind_paystack/src/config/retry_policy.dart';
6+
7+
class RetryInterceptor extends Interceptor {
8+
final Dio dio;
9+
final RetryPolicy retryPolicy;
10+
final void Function(String message)? logger;
11+
final _pendingRequests = <String, Completer<void>>{};
12+
13+
RetryInterceptor({
14+
required this.dio,
15+
required this.retryPolicy,
16+
this.logger,
17+
});
18+
19+
@override
20+
Future<void> onError(
21+
DioException err,
22+
ErrorInterceptorHandler handler,
23+
) async {
24+
final options = err.requestOptions;
25+
final requestKey = _getRequestKey(options);
26+
27+
// Check if we should retry
28+
if (!_shouldRetry(err)) {
29+
return handler.next(err);
30+
}
31+
32+
// Get current attempt from extra data
33+
final attempt = (options.extra['attempt'] ?? 0) as int;
34+
35+
if (attempt >= retryPolicy.maxAttempts) {
36+
_log('Max retry attempts reached for request: ${options.path}');
37+
return handler.next(err);
38+
}
39+
40+
// Check if request is already being retried
41+
if (_pendingRequests.containsKey(requestKey)) {
42+
try {
43+
await _pendingRequests[requestKey]!.future;
44+
// Previous retry succeeded, let's retry this one too
45+
return _retry(err, handler, attempt);
46+
} catch (_) {
47+
// Previous retry failed, fail this one too
48+
return handler.next(err);
49+
}
50+
}
51+
52+
return _retry(err, handler, attempt);
53+
}
54+
55+
Future<void> _retry(
56+
DioException err,
57+
ErrorInterceptorHandler handler,
58+
int attempt,
59+
) async {
60+
final options = err.requestOptions;
61+
final requestKey = _getRequestKey(options);
62+
final completer = Completer<void>();
63+
_pendingRequests[requestKey] = completer;
64+
65+
try {
66+
// Calculate delay for this attempt
67+
final delay = retryPolicy.getDelayForAttempt(attempt + 1);
68+
69+
_log(
70+
'Retrying request to ${options.path} after ${delay.inMilliseconds}ms '
71+
'(Attempt ${attempt + 1}/${retryPolicy.maxAttempts})');
72+
73+
// Wait for delay
74+
await Future.delayed(delay);
75+
76+
// Create new request options
77+
final newOptions = Options(
78+
method: options.method,
79+
headers: options.headers,
80+
extra: {
81+
...options.extra,
82+
'attempt': attempt + 1,
83+
},
84+
);
85+
86+
// Make the retry request
87+
final response = await dio.request<dynamic>(
88+
options.path,
89+
data: options.data,
90+
queryParameters: options.queryParameters,
91+
options: newOptions,
92+
);
93+
94+
completer.complete();
95+
handler.resolve(response);
96+
} catch (e) {
97+
completer.completeError(e);
98+
handler.next(err);
99+
} finally {
100+
_pendingRequests.remove(requestKey);
101+
}
102+
}
103+
104+
bool _shouldRetry(DioException err) {
105+
// Don't retry if request was cancelled
106+
if (err.type == DioExceptionType.cancel) {
107+
return false;
108+
}
109+
110+
// Retry on connection errors
111+
if (err.type == DioExceptionType.connectionTimeout ||
112+
err.type == DioExceptionType.receiveTimeout ||
113+
err.type == DioExceptionType.sendTimeout) {
114+
return true;
115+
}
116+
117+
// Retry on server errors
118+
if (err.response != null) {
119+
return retryPolicy.retryStatusCodes.contains(err.response!.statusCode);
120+
}
121+
122+
// Retry on network errors
123+
return err.type == DioExceptionType.connectionError;
124+
}
125+
126+
String _getRequestKey(RequestOptions options) {
127+
return '${options.method}:${options.path}';
128+
}
129+
130+
void _log(String message) {
131+
logger?.call(message);
132+
}
133+
134+
/// Helper method to get retry attempt from options
135+
static int getCurrentAttempt(RequestOptions options) {
136+
return (options.extra['attempt'] ?? 0) as int;
137+
}
138+
139+
/// Helper method to check if request can be retried
140+
static bool canRetry(RequestOptions options, RetryPolicy policy) {
141+
final attempt = getCurrentAttempt(options);
142+
return attempt < policy.maxAttempts;
143+
}
144+
}
145+
146+
/// Extension to add retry functionality to Dio
147+
extension RetryDioExtension on Dio {
148+
void addRetryPolicy(RetryPolicy policy, {void Function(String)? logger}) {
149+
interceptors.add(RetryInterceptor(
150+
dio: this,
151+
retryPolicy: policy,
152+
logger: logger,
153+
));
154+
}
155+
}

0 commit comments

Comments
 (0)