105 lines
3.2 KiB
Dart
105 lines
3.2 KiB
Dart
import 'package:dio/dio.dart';
|
|
import 'package:uuid/uuid.dart';
|
|
|
|
/// Interceptor yang secara otomatis menambahkan header X-Idempotency-Key
|
|
/// untuk endpoint-endpoint kritis yang memerlukan idempotency.
|
|
///
|
|
/// Key sebaiknya di-generate di level BLoC/Cubit saat user trigger action,
|
|
/// lalu dikirim via headers. Interceptor ini hanya berfungsi sebagai fallback
|
|
/// jika key belum di-set.
|
|
///
|
|
/// Jika response 409 dengan error code `request_in_progress`, interceptor
|
|
/// akan retry setelah delay 1-2 detik.
|
|
class IdempotencyInterceptor extends Interceptor {
|
|
static const _headerKey = 'X-Idempotency-Key';
|
|
static const _replayHeader = 'X-Idempotent-Replay';
|
|
|
|
// Endpoints that require idempotency keys (exact match)
|
|
static const _idempotentPaths = [
|
|
'/api/v1/payments',
|
|
'/api/v1/orders/void',
|
|
];
|
|
|
|
// Endpoints that require idempotency keys (pattern match)
|
|
static final _idempotentPathPatterns = [
|
|
RegExp(r'/api/v1/orders/.+/add-items$'),
|
|
RegExp(r'/api/v1/orders/.+/refund$'),
|
|
RegExp(r'/api/v1/payments/.+/refund$'),
|
|
];
|
|
|
|
static const _maxRetries = 3;
|
|
static const _retryDelay = Duration(seconds: 2);
|
|
|
|
final Dio _dio;
|
|
|
|
IdempotencyInterceptor(this._dio);
|
|
|
|
@override
|
|
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
|
if (options.method == 'POST' && _requiresIdempotencyKey(options.path)) {
|
|
// Gunakan key yang sudah di-set dari BLoC, atau generate baru sebagai fallback
|
|
options.headers[_headerKey] ??= const Uuid().v4();
|
|
}
|
|
handler.next(options);
|
|
}
|
|
|
|
@override
|
|
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
|
// Log jika response adalah replay dari request sebelumnya
|
|
final isReplay = response.headers.value(_replayHeader);
|
|
if (isReplay == 'true') {
|
|
// Response ini adalah hasil dari request pertama yang sudah diproses.
|
|
// Treat as success — tidak perlu perlakuan khusus.
|
|
}
|
|
handler.next(response);
|
|
}
|
|
|
|
@override
|
|
void onError(DioException err, ErrorInterceptorHandler handler) async {
|
|
final response = err.response;
|
|
final options = err.requestOptions;
|
|
|
|
if (response == null) {
|
|
handler.next(err);
|
|
return;
|
|
}
|
|
|
|
// Handle 409 Conflict with request_in_progress
|
|
if (response.statusCode == 409 &&
|
|
_isRequestInProgress(response.data) &&
|
|
_requiresIdempotencyKey(options.path)) {
|
|
final retryCount = options.extra['_idempotency_retry_count'] ?? 0;
|
|
|
|
if (retryCount < _maxRetries) {
|
|
await Future.delayed(_retryDelay);
|
|
|
|
options.extra['_idempotency_retry_count'] = retryCount + 1;
|
|
|
|
try {
|
|
final retryResponse = await _dio.fetch(options);
|
|
handler.resolve(retryResponse);
|
|
} on DioException catch (e) {
|
|
handler.next(e);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
handler.next(err);
|
|
}
|
|
|
|
bool _requiresIdempotencyKey(String path) {
|
|
if (_idempotentPaths.any((p) => path.endsWith(p))) return true;
|
|
if (_idempotentPathPatterns.any((r) => r.hasMatch(path))) return true;
|
|
return false;
|
|
}
|
|
|
|
bool _isRequestInProgress(dynamic data) {
|
|
if (data is Map<String, dynamic>) {
|
|
final errorCode = data['error_code'] ?? data['code'] ?? '';
|
|
return errorCode == 'request_in_progress';
|
|
}
|
|
return false;
|
|
}
|
|
}
|