fix: handle corrupted server response in GlossarySyncService (Server error appended to dump body)

This commit is contained in:
mso7nueh
2026-04-22 17:08:26 +03:00
parent 8cdce6a885
commit 10c7e0a584

View File

@@ -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});
}