From 10c7e0a584bd1d904d08eade8a280e790efd8b48 Mon Sep 17 00:00:00 2001 From: mso7nueh Date: Wed, 22 Apr 2026 17:08:26 +0300 Subject: [PATCH] fix: handle corrupted server response in GlossarySyncService (Server error appended to dump body) --- .../services/glossary_sync_runner.dart | 79 +++++++++++++++++-- 1 file changed, 74 insertions(+), 5 deletions(-) diff --git a/lib/core/local_db/services/glossary_sync_runner.dart b/lib/core/local_db/services/glossary_sync_runner.dart index 48326678..6edde2c5 100644 --- a/lib/core/local_db/services/glossary_sync_runner.dart +++ b/lib/core/local_db/services/glossary_sync_runner.dart @@ -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; - final dataList = json['data'] as List; + 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 json; + try { + json = jsonDecode(content) as Map; + } catch (e) { + debugPrint('[GlossarySyncService] Не удалось распарсить дамп: $e'); + return _IsolateResult(count: 0, updatedAt: null, isServerError: true); + } + + final dataList = (json['data'] as List?) ?? []; 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}); } -