Skip to content

Commit b96a79a

Browse files
committed
fix: improve retry policies, url parsing and documentation
1 parent 96aa719 commit b96a79a

File tree

4 files changed

+110
-5
lines changed

4 files changed

+110
-5
lines changed

README.md

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,11 +227,25 @@ Sometimes you need to retry a request due to different circumstances, an expired
227227

228228
```dart
229229
class ExpiredTokenRetryPolicy extends RetryPolicy {
230+
@override
231+
int get maxRetryAttempts => 2;
232+
233+
@override
234+
bool shouldAttemptRetryOnException(Exception reason, BaseRequest request) {
235+
// Log the exception for debugging
236+
print('Request failed: ${reason.toString()}');
237+
print('Request URL: ${request.url}');
238+
239+
// Retry on network exceptions, but not on client errors
240+
return reason is SocketException || reason is TimeoutException;
241+
}
242+
230243
@override
231244
Future<bool> shouldAttemptRetryOnResponse(BaseResponse response) async {
232245
if (response.statusCode == 401) {
233246
// Perform your token refresh here.
234-
247+
print('Token expired, refreshing...');
248+
235249
return true;
236250
}
237251
@@ -242,13 +256,56 @@ class ExpiredTokenRetryPolicy extends RetryPolicy {
242256

243257
You can also set the maximum amount of retry attempts with `maxRetryAttempts` property or override the `shouldAttemptRetryOnException` if you want to retry the request after it failed with an exception.
244258

259+
### RetryPolicy Interface
260+
261+
The `RetryPolicy` abstract class provides the following methods that you can override:
262+
263+
- **`shouldAttemptRetryOnException(Exception reason, BaseRequest request)`**: Called when an exception occurs during the request. Return `true` to retry, `false` to fail immediately.
264+
- **`shouldAttemptRetryOnResponse(BaseResponse response)`**: Called after receiving a response. Return `true` to retry, `false` to accept the response.
265+
- **`maxRetryAttempts`**: The maximum number of retry attempts (default: 1).
266+
- **`delayRetryAttemptOnException({required int retryAttempt})`**: Delay before retrying after an exception (default: no delay).
267+
- **`delayRetryAttemptOnResponse({required int retryAttempt})`**: Delay before retrying after a response (default: no delay).
268+
269+
### Using Retry Policies
270+
271+
To use a retry policy, pass it to the `InterceptedClient` or `InterceptedHttp`:
272+
273+
```dart
274+
final client = InterceptedClient.build(
275+
interceptors: [WeatherApiInterceptor()],
276+
retryPolicy: ExpiredTokenRetryPolicy(),
277+
);
278+
```
279+
245280
Sometimes it is helpful to have a cool-down phase between multiple requests. This delay could for example also differ between the first and the second retry attempt as shown in the following example.
246281

247282
```dart
248283
class ExpiredTokenRetryPolicy extends RetryPolicy {
284+
@override
285+
int get maxRetryAttempts => 3;
286+
287+
@override
288+
bool shouldAttemptRetryOnException(Exception reason, BaseRequest request) {
289+
// Only retry on network-related exceptions
290+
return reason is SocketException || reason is TimeoutException;
291+
}
292+
293+
@override
294+
Future<bool> shouldAttemptRetryOnResponse(BaseResponse response) async {
295+
// Retry on server errors (5xx) and authentication errors (401)
296+
return response.statusCode >= 500 || response.statusCode == 401;
297+
}
298+
299+
@override
300+
Duration delayRetryAttemptOnException({required int retryAttempt}) {
301+
// Exponential backoff for exceptions
302+
return Duration(milliseconds: (250 * math.pow(2.0, retryAttempt - 1)).round());
303+
}
304+
249305
@override
250306
Duration delayRetryAttemptOnResponse({required int retryAttempt}) {
251-
return const Duration(milliseconds: 250) * math.pow(2.0, retryAttempt);
307+
// Exponential backoff for response-based retries
308+
return Duration(milliseconds: (250 * math.pow(2.0, retryAttempt - 1)).round());
252309
}
253310
}
254311
```

lib/models/retry_policy.dart

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import 'package:http/http.dart';
1212
/// int get maxRetryAttempts => 2;
1313
///
1414
/// @override
15-
/// bool shouldAttemptRetryOnException(Exception reason) {
15+
/// bool shouldAttemptRetryOnException(Exception reason, BaseRequest request) {
1616
/// log(reason.toString());
17+
/// log("Request URL: ${request.url}");
1718
///
1819
/// return false;
1920
/// }
@@ -36,20 +37,45 @@ import 'package:http/http.dart';
3637
abstract class RetryPolicy {
3738
/// Defines whether the request should be retried when an Exception occurs
3839
/// while making said request to the server.
40+
///
41+
/// [reason] - The exception that occurred during the request
42+
/// [request] - The original request that failed
43+
///
44+
/// Returns `true` if the request should be retried, `false` otherwise.
3945
FutureOr<bool> shouldAttemptRetryOnException(
4046
Exception reason, BaseRequest request) =>
4147
false;
4248

4349
/// Defines whether the request should be retried after the request has
4450
/// received `response` from the server.
51+
///
52+
/// [response] - The response received from the server
53+
///
54+
/// Returns `true` if the request should be retried, `false` otherwise.
55+
/// Common use cases include retrying on 401 (Unauthorized) or 500 (Server Error).
4556
FutureOr<bool> shouldAttemptRetryOnResponse(BaseResponse response) => false;
4657

4758
/// Number of maximum request attempts that can be retried.
59+
///
60+
/// Default is 1, meaning the original request plus 1 retry attempt.
61+
/// Set to 0 to disable retries, or higher values for more retry attempts.
4862
int get maxRetryAttempts => 1;
4963

64+
/// Delay before retrying when an exception occurs.
65+
///
66+
/// [retryAttempt] - The current retry attempt number (1-based)
67+
///
68+
/// Returns the delay duration. Default is no delay (Duration.zero).
69+
/// Consider implementing exponential backoff for production use.
5070
Duration delayRetryAttemptOnException({required int retryAttempt}) =>
5171
Duration.zero;
5272

73+
/// Delay before retrying when a response indicates retry is needed.
74+
///
75+
/// [retryAttempt] - The current retry attempt number (1-based)
76+
///
77+
/// Returns the delay duration. Default is no delay (Duration.zero).
78+
/// Consider implementing exponential backoff for production use.
5379
Duration delayRetryAttemptOnResponse({required int retryAttempt}) =>
5480
Duration.zero;
5581
}

lib/utils/query_parameters.dart

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,21 @@ String buildUrlString(String url, Map<String, dynamic>? parameters) {
99
// Check if there are parameters to add.
1010
if (parameters.isNotEmpty) {
1111
// Validate URL structure to prevent injection
12+
// First check if it looks like a valid HTTP/HTTPS URL
13+
if (!url.startsWith('http://') && !url.startsWith('https://')) {
14+
throw ArgumentError('Invalid URL structure: $url - must be a valid HTTP/HTTPS URL');
15+
}
16+
1217
try {
13-
Uri.parse(url);
18+
final uri = Uri.parse(url);
19+
// Additional validation: ensure it has a host
20+
if (uri.host.isEmpty) {
21+
throw ArgumentError('Invalid URL structure: $url - must have a valid host');
22+
}
1423
} catch (e) {
24+
if (e is ArgumentError) {
25+
rethrow;
26+
}
1527
throw ArgumentError('Invalid URL structure: $url');
1628
}
1729

test/utils/utils_test.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import 'package:test/test.dart';
21
import 'package:http_interceptor/utils/utils.dart';
2+
import 'package:test/test.dart';
33

44
main() {
55
group("buildUrlString", () {
@@ -73,5 +73,15 @@ main() {
7373
expect(() => buildUrlString(invalidUrl, parameters),
7474
throwsA(isA<ArgumentError>()));
7575
});
76+
77+
test("Validates URL structure and throws error for URLs without scheme", () {
78+
// Arrange
79+
String invalidUrl = "example.com/path"; // No scheme
80+
Map<String, dynamic> parameters = {"key": "value"};
81+
82+
// Act & Assert
83+
expect(() => buildUrlString(invalidUrl, parameters),
84+
throwsA(isA<ArgumentError>()));
85+
});
7686
});
7787
}

0 commit comments

Comments
 (0)