forked from eatvio/eatvio_app
fix: handle corrupted server response in GlossarySyncService (Server error appended to dump body)
This commit is contained in:
@@ -77,6 +77,12 @@ class GlossarySyncService {
|
||||
_IsolateArgs(filePath: dumpFile.path, token: token),
|
||||
);
|
||||
|
||||
if (result.isServerError) {
|
||||
// Сервер вернул ошибку внутри стрима — дамп неполный, пропускаем
|
||||
debugPrint('[GlossarySyncService] Сервер вернул ошибку в теле дампа, синхронизация пропущена');
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Шаг 6: Сохранить актуальные meta-данные
|
||||
await _localDataSource.saveGlossaryMeta(
|
||||
checksum: meta.checksum,
|
||||
@@ -103,9 +109,33 @@ class GlossarySyncService {
|
||||
// Инициализируем platform channels в изоляте (нужно для drift_flutter)
|
||||
BackgroundIsolateBinaryMessenger.ensureInitialized(args.token);
|
||||
|
||||
final content = File(args.filePath).readAsStringSync();
|
||||
final json = jsonDecode(content) as Map<String, dynamic>;
|
||||
final dataList = json['data'] as List<dynamic>;
|
||||
final file = File(args.filePath);
|
||||
final rawContent = file.readAsStringSync();
|
||||
|
||||
// ─── Защита от «склеенных» ответов сервера ─────────────────────────────
|
||||
// Сервер иногда падает в процессе стриминга дампа и дописывает
|
||||
// {"message":"Server error.",...} прямо в тело — без разделителя.
|
||||
// Пример: ...,"deleted_at":null}{"message":"Server error.",...}
|
||||
// Такой контент невалиден как JSON целиком, поэтому:
|
||||
// 1. Обнаруживаем склейку по паттерну `}{`
|
||||
// 2. Обрезаем строку до первого закрывающего `}` корневого объекта.
|
||||
// 3. Если файл не парсится вообще — возвращаем isServerError=true.
|
||||
final content = _sanitizeJsonResponse(rawContent);
|
||||
if (content == null) {
|
||||
debugPrint('[GlossarySyncService] Файл дампа повреждён (невозможно восстановить JSON)');
|
||||
return _IsolateResult(count: 0, updatedAt: null, isServerError: true);
|
||||
}
|
||||
// ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
final Map<String, dynamic> json;
|
||||
try {
|
||||
json = jsonDecode(content) as Map<String, dynamic>;
|
||||
} catch (e) {
|
||||
debugPrint('[GlossarySyncService] Не удалось распарсить дамп: $e');
|
||||
return _IsolateResult(count: 0, updatedAt: null, isServerError: true);
|
||||
}
|
||||
|
||||
final dataList = (json['data'] as List<dynamic>?) ?? [];
|
||||
final updatedAt = json['updated_at']?.toString();
|
||||
|
||||
if (dataList.isEmpty) return _IsolateResult(count: 0, updatedAt: updatedAt);
|
||||
@@ -147,6 +177,44 @@ class GlossarySyncService {
|
||||
await db.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// Обрезает невалидный «склеенный» JSON до первой валидной части.
|
||||
///
|
||||
/// Сервер иногда дописывает ошибку после корректного JSON:
|
||||
/// `...null}{"message":"Server error."}` — это два объекта подряд.
|
||||
/// Метод находит границу и возвращает только первый объект.
|
||||
/// Если восстановить невозможно — возвращает null.
|
||||
static String? _sanitizeJsonResponse(String raw) {
|
||||
// Быстрый путь: если нет признаков склейки — возвращаем как есть
|
||||
if (!raw.contains('}\u007b') && !_hasServerErrorMarker(raw)) {
|
||||
return raw;
|
||||
}
|
||||
|
||||
// Ищем паттерн конца первого объекта + начала второго: `}{`
|
||||
// с возможными пробелами между ними: `} {` или `}\n{`
|
||||
final splicePattern = RegExp(r'\}\s*\{');
|
||||
final match = splicePattern.firstMatch(raw);
|
||||
if (match != null) {
|
||||
// Берём только часть до начала второго объекта
|
||||
final truncated = raw.substring(0, match.start + 1);
|
||||
debugPrint('[GlossarySyncService] Обнаружена склейка на позиции ${match.start}, обрезаем дамп');
|
||||
return truncated;
|
||||
}
|
||||
|
||||
// Нет явной границы, но контент содержит признаки ошибки — не парсим
|
||||
if (_hasServerErrorMarker(raw)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
static bool _hasServerErrorMarker(String content) {
|
||||
return content.contains('"Server error"') ||
|
||||
content.contains('"Server Error"') ||
|
||||
// Проверяем наличие ключей типичного error-ответа не в начале строки
|
||||
(content.indexOf('"message"') > 1000 && content.contains('"error"'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Аргументы для передачи в compute() изолят.
|
||||
@@ -160,6 +228,7 @@ class _IsolateArgs {
|
||||
class _IsolateResult {
|
||||
final int count;
|
||||
final String? updatedAt;
|
||||
const _IsolateResult({required this.count, this.updatedAt});
|
||||
/// true если сервер вернул ошибку внутри стрима дампа
|
||||
final bool isServerError;
|
||||
const _IsolateResult({required this.count, this.updatedAt, this.isServerError = false});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user