Skip to content

Commit 4464d14

Browse files
SERDUNCopilot
andauthored
feat: add RetryInterceptor for AppResourcesGetCommand server error handling (#36)
- Add RetryInterceptor Dio interceptor with exponential backoff (3 retries at 2000ms, 4000ms, 8000ms) - Retry on HTTP >= 500 (server errors) and connection/timeout errors - Fail fast on HTTP 401 with 'Authentication token has expired.' exception - Add interceptors barrel file - Refactor command_runner.dart to use _buildDefaultDatasource() static method Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: SERDUN <26858237+SERDUN@users.noreply.github.com>
1 parent 3e67127 commit 4464d14

3 files changed

Lines changed: 78 additions & 12 deletions

File tree

lib/src/command_runner.dart

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:pub_updater/pub_updater.dart';
88
import 'package:webtrit_phone_tools/src/commands/commands.dart';
99
import 'package:webtrit_phone_tools/src/version.dart';
1010

11+
import 'commands/app_resources/interceptors/interceptors.dart';
1112
import 'commands/constants.dart';
1213
import 'utils/utils.dart';
1314

@@ -32,18 +33,7 @@ class WebtritPhoneToolsCommandRunner extends CompletionCommandRunner<int> {
3233
PubUpdater? pubUpdater,
3334
}) : _logger = logger ?? Logger(),
3435
_httpClient = httpClient ?? HttpClient(configuratorApiUrl, Logger()),
35-
_datasource = datasource ??
36-
ConfiguratorBackandDatasource(
37-
Dio(BaseOptions(baseUrl: 'https://us-central1-webtrit-configurator.cloudfunctions.net/api/v1'))
38-
..interceptors.add(
39-
LogInterceptor(
40-
requestBody: true,
41-
responseBody: true,
42-
logPrint: print,
43-
),
44-
),
45-
UnauthorizedInterceptor(),
46-
),
36+
_datasource = datasource ?? _buildDefaultDatasource(),
4737
_keystoreReadmeUpdater = keystoreReadmeUpdater ?? KeystoreReadmeUpdater(Logger()),
4838
_pubUpdater = pubUpdater ?? PubUpdater(),
4939
super(executableName, description) {
@@ -79,6 +69,17 @@ class WebtritPhoneToolsCommandRunner extends CompletionCommandRunner<int> {
7969
addCommand(UpdateCommand(logger: _logger, pubUpdater: _pubUpdater));
8070
}
8171

72+
static ConfiguratorBackandDatasource _buildDefaultDatasource() {
73+
final dio = Dio(BaseOptions(baseUrl: 'https://us-central1-webtrit-configurator.cloudfunctions.net/api/v1'))
74+
..interceptors.add(LogInterceptor(
75+
requestBody: true,
76+
responseBody: true,
77+
logPrint: print,
78+
));
79+
dio.interceptors.add(RetryInterceptor(dio: dio));
80+
return ConfiguratorBackandDatasource(dio, UnauthorizedInterceptor());
81+
}
82+
8283
@override
8384
void printUsage() => _logger.info(usage);
8485

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export 'retry_interceptor.dart';
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import 'package:data/datasource/datasource.dart';
2+
3+
class RetryInterceptor extends Interceptor {
4+
RetryInterceptor({
5+
required Dio dio,
6+
int maxRetries = 3,
7+
List<int> retryDelaysMs = const [2000, 4000, 8000],
8+
}) : assert(retryDelaysMs.length >= maxRetries, 'retryDelaysMs must contain at least maxRetries entries'),
9+
_dio = dio,
10+
_maxRetries = maxRetries,
11+
_retryDelaysMs = retryDelaysMs;
12+
13+
static const _retryCountKey = 'retryCount';
14+
15+
final Dio _dio;
16+
final int _maxRetries;
17+
final List<int> _retryDelaysMs;
18+
19+
@override
20+
void onError(DioException err, ErrorInterceptorHandler handler) => _handleError(err, handler);
21+
22+
Future<void> _handleError(DioException err, ErrorInterceptorHandler handler) async {
23+
try {
24+
if (_isUnauthorized(err)) {
25+
handler.reject(_buildUnauthorizedException(err));
26+
return;
27+
}
28+
29+
final retryCount = (err.requestOptions.extra[_retryCountKey] as int?) ?? 0;
30+
31+
if (!_shouldRetry(err) || retryCount >= _maxRetries) {
32+
handler.next(err);
33+
return;
34+
}
35+
36+
await Future<void>.delayed(Duration(milliseconds: _retryDelaysMs[retryCount]));
37+
err.requestOptions.extra[_retryCountKey] = retryCount + 1;
38+
handler.resolve(await _dio.fetch(err.requestOptions));
39+
} on DioException catch (e) {
40+
handler.next(e);
41+
} catch (_) {
42+
handler.next(err);
43+
}
44+
}
45+
46+
bool _isUnauthorized(DioException err) => err.response?.statusCode == 401;
47+
48+
bool _shouldRetry(DioException err) => _isServerError(err) || _isConnectionError(err);
49+
50+
bool _isServerError(DioException err) => (err.response?.statusCode ?? 0) >= 500;
51+
52+
bool _isConnectionError(DioException err) =>
53+
err.type == DioExceptionType.connectionTimeout ||
54+
err.type == DioExceptionType.sendTimeout ||
55+
err.type == DioExceptionType.receiveTimeout ||
56+
err.type == DioExceptionType.connectionError;
57+
58+
DioException _buildUnauthorizedException(DioException err) => DioException(
59+
requestOptions: err.requestOptions,
60+
response: err.response,
61+
type: err.type,
62+
error: Exception('Authentication token has expired.'),
63+
);
64+
}

0 commit comments

Comments
 (0)