Не вдалося розпарсити відповідь: причины и как исправить

Сообщение «Не вдалося розпарсити відповідь» появляется в тот момент, когда программа уже получила данные, но не сумела превратить их в понятную структуру. Чаще всего клиент ждал JSON, а на вход попал HTML, пустая строка, обрезанный пакет или текст с «грязными» кавычками. Для кассира это выглядит как отказ терминала, для разработчика — как SyntaxError, для бухгалтера — как зависшая оплата.

Разбор ответа — не магия и не «поломка интернета». Парсер идёт по строке символ за символом и срывается на первом нарушении грамматики. Починка начинается с простой проверки: какой именно текст пришёл, с какого символа он стартует и совпадает ли заголовок Content-Type с реальным содержимым.

Ниже — рабочая карта ошибки: от кассовых программ вроде Торгсофт до fetch() в браузере и json_decode() в PHP. Разберём причины, симптомы, диагностику и конкретные шаги, которые закрывают проблему и у пользователей, и у тех, кто пишет интеграции.

Что на самом деле означает эта ошибка

Парсинг — это перевод сырого текста в объекты, массивы и числа. Программа не «читает глазами», она применяет жёсткие правила. JSON, описанный в RFC 8259 (декабрь 2017, rfc-editor.org), требует UTF-8, двойные кавычки, запрещает комментарии и висячие запятые. Один лишний символ — и разбор останавливается.

Фраза «Не вдалося розпарсити відповідь» — украинская формулировка того же сбоя, который в логах Chrome выглядит как Unexpected token, в PHP — как JSON_ERROR_SYNTAX, в Python — как json.JSONDecodeError. Смысл один: вход не соответствует ожидаемому формату. Сервер мог ответить корректно по HTTP, но тело ответа для парсера — мусор.

Самое важное: ошибка почти никогда не говорит «сервер умер». Она говорит «я не понял, что мне прислали». Пока вы не посмотрите тело ответа, любые перезагрузки — лотерея.

В нашей практике мы сталкивались с таким случаем, когда касса честно писала про разбор JSON, а в трафике лежала HTML-страница «502 Bad Gateway» от прокси. Кассир видел отказ оплаты, программист — «сломанный терминал», а чинился обычный nginx, который вместо JSON отдал свою заглушку. Путаница дорого стоит в час пик: очередь, повторные списания, нервы.

Как устроен разбор ответа и где он ломается

Клиент отправляет запрос. Сервер отвечает статусом, заголовками и телом. Приложение смотрит на договорённость — обычно «будет JSON» — и кормит тело в JSON.parse(), json_decode() или штатный разборщик учётки. Если грамматика нарушена, библиотека бросает исключение, а интерфейс показывает уже человекочитаемую фразу про неудачный разбор.

Между «отправили» и «разобрали» сидит ещё транспорт: TLS, прокси, CDN, антивирус, корпоративный фильтр, драйвер COM-порта, WebSocket-мост к POS-терминалу. Любой из них может подменить тело. Поэтому чинить только «код на клиенте» — половина работы. Вторая половина — выяснить, кто испортил байты по дороге.

По моему опыту сопровождения кассовых интеграций, парсер падает в трёх семьях ситуаций. Первая — вместо JSON пришёл другой язык: HTML, XML, обычный текст. Вторая — JSON есть, но он калечный: кавычки, запятые, кодировка. Третья — тела нет вовсе: таймаут, обрыв TCP, пустой 204, ответ 0 байт после редиректа на логин.

JSON — капризный, и это его сила

Формат специально узкий. Ключи только в двойных кавычках. Строки — тоже. Одинарные кавычки, как в JavaScript, запрещены. Висячая запятая после последнего поля, привычная в современном JS, для JSON — приговор. Комментарии // и /* */ парсер не прощает. Значения NaN, Infinity и undefined в стандарт не входят.

Документация MDN по JSON.parse прямо показывает: массив [1, 2, 3, 4,] и объект {«foo»: 1,} дают SyntaxError. То же с {‘foo’: 1}. Это не «баг браузера», это спецификация. Кто генерирует ответ руками, через конкатенацию строк, рано или поздно попадёт в эту ловушку. Сериализатор языка надёжнее ручной склейки всегда.

Типичные причины, из-за которых ответ не разбирается

Список ниже собран из реальных инцидентов: веб-приложения, REST API, учётные системы, банковские терминалы. Перед ним коротко: сначала смотрите первый символ тела, потом код статуса, потом заголовок Content-Type. Эта тройка закрывает большую часть случаев без шаманства.

  • HTML вместо JSON. Парсер видит < и падает с Unexpected token ‘<‘ at position 0. Обычно это

    : страница 404, 500, логин, заглушка Cloudflare, ответ phpinfo, админский «friendly error». Приложение ждало объект, получило сайт.

  • Пустое или обрезанное тело. JSON.parse(») и разбор пробелов дают Unexpected end of JSON input. В PHP 7 и новее json_decode(«») ставит JSON_ERROR_SYNTAX. Обрыв бывает при таймауте, убитом воркере, лимите прокси, обрыве USB у терминала.
  • Неэкранированные кавычки в данных. Классика касс: в названии торговой точки, товара или улицы стоит символ «. Если драйвер вставил его в JSON без экранирования, строка рвётся посередине. В Торгсофт версия 2022.0.36 как раз чинила разбор ответа терминала при двойных кавычках в названии точки (torgsoft.ua).
  • BOM и «невидимые» байты. UTF-8 BOM (U+FEFF) в начале файла выглядит пустым местом, но для строгого парсера это мусор на позиции 0. RFC 8259 запрещает добавлять BOM в сетевой JSON; игнорировать его парсеру только разрешено, не предписано.
  • Чужая кодировка. JSON по стандарту — UTF-8. Windows-1251, CP866 и «как получилось в Excel» ломают многобайтовые символы. Тогда всплывает JSON_ERROR_UTF8 или «Unexpected token» на середине кириллицы.
  • Редирект на логин. Сессия истекла, API вернул 302, клиент послушно пошёл на HTML-форму. Статус уже 200, Content-Type — text/html, а код всё равно зовёт response.json(). Ошибка выглядит как «сломанный API», хотя сломалась авторизация.
  • Склеенные документы. Два объекта подряд {«id»:1}{«id»:2} — это не массив. NDJSON так устроен намеренно, обычный JSON.parse — нет. Похожая беда бывает, когда лог и полезная нагрузка пишутся в один сокет.

Отдельно стоит ручная сборка JSON через интерполяцию строк. Один перевод строки из CSV, одна типографская кавычка «ёлочкой» из Word, один неэкранированный апостроф в украинской фамилии — и разбор падает. Сериализуйте структурами языка, не склеивайте кавычки вручную.

Где ошибка всплывает чаще всего

Одна и та же формулировка живёт в разных мирах. Кассиру она знакома по учёткам и банковским терминалам, фронтендеру — по DevTools, бэкендеру — по логам PHP и Node. Сценарии разные, механика общая.

Касса, Торгсофт и JSON-терминалы

Украинские торговые программы часто общаются с POS по протоколу JSON: USB, COM, Ethernet, WebSocket. ПриватБанк описывает JSON-протокол как универсальный кассовый слой для Ingenico, Verifone, PAX, Newland и других моделей. Терминал отвечает пакетом, касса его разбирает. Если в поле названия точки, мерчанта или товара попал спецсимвол, драйвер отдаёт битую строку — и на экране появляется «Не вдалося розпарсити відповідь» или её JSON-вариант.

В версии Торгсофт 2022.0.36 отдельно починили обработку ответа терминала, когда в названии торговой точки стояли двойные кавычки. До патча оплата могла сыпаться при живой связи с банком. Если ошибка сидит у вас до сих пор, проверьте номер сборки, логи драйвера (часто trace.log рядом с genericDriverJsonETH.exe / genericDriverJsonUSB.exe) и то, нет ли в реквизитах точки кавычек, амперсандов и переносов строк.

Похожий сюжет есть у соседних учёток: «Value is null => The JSON string is invalid» и совет обратиться в банк, потому что в названии торговой точки недопустимые символы. Касса здесь ни при чём — ломается контракт с эквайером.

Браузер, fetch и мобильные клиенты

Вызов response.json() без проверки статуса — главный поставщик Unexpected token ‘<‘. Сервер отдал 404-страницу nginx, фронт всё равно парсит. Axios в дефолте делает то же самое, если перехватчик не смотрит на Content-Type. В Progressive Web App к этому добавляется Service Worker, который может отдать закэшированный HTML.

Пустой ответ после DELETE или 204 No Content ловит ту же семью: тело нулевое, json() падает. Для «нет содержимого» нужно не парсить, а проверять status и длину. Мы не раз чинили дашборды, где «пропали данные» означало всего лишь, что бэкенд сменил 200+{} на 204.

PHP, 1С и внутренние сервисы

json_decode() молча возвращает null и при ошибке, и при валидном JSON null. Без json_last_error() вы не отличите «пришёл null» от «пришла каша». Константа JSON_ERROR_SYNTAX равна 4, JSON_ERROR_UTF8 — 5, JSON_ERROR_CTRL_CHAR — 3. Пустая строка в PHP 7+ — синтаксическая ошибка, в PHP 5 парсер вёл себя мягче. Старые интеграции, переехавшие на новый интерпретатор, внезапно «ломаются» без изменения JSON.

В 1С и похожих платформах разбор падает на именах свойств с дефисом, на BOM из выгрузки Excel и на кириллице в неверной кодовой странице. Симптом тот же: «не удалось прочитать JSON», хотя файл «открывается в блокноте нормально». Блокнот врёт: он не парсер.

Быстрая диагностика: что смотреть в первую очередь

Десять минут с холодной головой экономят день переустановок. Ниже — порядок, который я гоняю сам, от кассы до SPA. Сначала факты, потом гипотезы.

  1. Скопируйте точный текст ошибки и время. Нужны секунды, касса, операция. «Оно иногда бывает» без штампа времени — гадание.
  2. Посмотрите сырое тело, не «как кажется». В браузере — вкладка Network, Response. На кассе — лог драйвера терминала. На сервере — tcpdump, журнал nginx, лог приложения. Ищите первый символ.
  3. Сверьте HTTP-статус и Content-Type. 200 + application/json — одно. 200 + text/html — другое. 302, 401, 502, 504 почти всегда объясняют HTML в теле.
  4. Проверьте длину. 0 байт, обрыв на полуслове, Content-Length больше фактического тела — режьте таймауты, буферы прокси, лимиты PHP post_max_size и nginx proxy_read_timeout.
  5. Прогоните фрагмент через валидатор. Если кусок валиден, а целиком нет — виноваты обёртка, BOM, два документа подряд или мусор в хвосте (PHP Notice, debug echo, BOM редактора).

Первый символ тела — лучший детектор. Открывающая угловая скобка почти всегда HTML. Открывающая фигурная или квадратная — вы в JSON, копайте синтаксис. Цифра, кавычка или минус — возможно, примитив, и клиент ждал объект. Буква u в начале часто выдаёт undefined, случайно попавший в сериализацию.

</html

Что видите в теле Типичный сигнал парсера Куда копать Первый шаг

или <html

Unexpected token ‘<‘ 404, 500, логин, прокси Проверить URL, статус, авторизацию
Пустая строка или одни пробелы Unexpected end of JSON input Таймаут, 204, обрыв канала Не парсить пустое тело, поднять таймаут
JSON с «внутри строки Unexpected token посреди текста Название точки, товара, адрес Экранировать или убрать кавычки в справочнике
Невидимый BOM, затем { Unexpected token на позиции 0 Выгрузка из Windows-редактора Сохранить UTF-8 без BOM
Кракозябры на месте кириллицы JSON_ERROR_UTF8 / битый символ Windows-1251, «битый» конвертер Привести канал к UTF-8 целиком

Таблица опирается на поведение JSON.parse в движках V8/SpiderMonkey и на коды json_last_error() в PHP. Формулировки сообщений чуть гуляют между браузерами, смысл позиции — нет: номер в Unexpected token — смещение в символах исходной строки, не номер строки файла.

Практическое лечение для пользователей программ

Если вы не пишете код, а работаете в кассе, CRM или личном кабинете, не нужно учить RFC. Нужно сузить круг: это разовая сеть, кривые реквизиты или баг версии. Идите от простого к сложному, не переустанавливая Windows на первом же экране.

  1. Повторите операцию через минуту. Краткий обрыв терминала или 502 шлюза часто проходит. Если ошибка стабильна на одном и том же чеке — это не «моргание сети».
  2. Проверьте название торговой точки, товара, контрагента. Уберите двойные кавычки, «ёлочки», вертикальные черты, переносы. Сохраните, запросите у банка правку реквизита мерчанта, повторите оплату.
  3. Перезапустите драйвер терминала и кассу в правильном порядке. Сначала мост JSON (generic-драйвер, WebSocket-сервис), потом программу учёта. Занятый COM-порт или второй экземпляр драйвера даёт рваные ответы, которые касса честно не разбирает.
  4. Обновите программу до сборки, где баг закрыт. Для Торгсофт ориентир — 2022.0.36 и новее в ветке, которой вы пользуетесь. Не ставьте «случайный» патч поверх пиратской копии: драйверы банка к этому чувствительны.
  5. Снимите лог и отдайте его в поддержку целиком. Нужны время ошибки, сумма, номер терминала, файл trace.log. Без сырого ответа вам ответят шаблоном «перезагрузите».

Отдельный бытовой сюжет — антивирус, который подменяет HTTPS своим сертификатом и вставляет HTML-заглушку «сайт заблокирован». Для кассы это выглядит как сломанный банк. Временное исключение для каталога программы и драйвера терминала быстро показывает, виноват ли фильтр.

Не проводите повторную оплату вслепую. Если разбор упал после того, как терминал уже ответил банку, деньги могли уйти. Сначала сверка по RRN и слипу, потом новый чек.

Исправление на стороне разработчика

Код, который слепо зовёт JSON.parse(await res.text()), сам создаёт эту ошибку. Нормальный клиент проверяет статус, тип содержимого и длину, и только потом разбирает. Ниже — устойчивый каркас, который я ставлю в сервисы, где цена простоя — очередь у кассы.

Порядок обработки HTTP-ответа

Сначала response.ok и код. Потом Content-Type: если там text/html, не кормите парсер, логируйте префикс тела. Для 204 и пустого 200 возвращайте null осознанно. Только после этого — parse. Обёртка try/catch обязательна, но она не лечит причину: в catch пишите первые 200 символов ответа, статус и URL. Без этого лог бесполезен.

Не глотайте ошибку пустым объектом {}. Молчаливый fallback превращает сбой авторизации в «пустой список товаров» и маскирует инцидент на недели. Лучше показать пользователю честный отказ и сохранить сырой фрагмент в журнал.

Генерация JSON только сериализатором

Никакой склейки ‘{«name»:»‘ + title + ‘»}’. В JavaScript — JSON.stringify, в PHP — json_encode с JSON_UNESCAPED_UNICODE и проверкой json_last_error(), в Python — json.dumps с ensure_ascii=False и utf-8. Кавычки, перевод строки, суррогатные пары и обратный слэш экранируются сами. Руками вы это воспроизведёте ровно до первого товара с дюймами в названии.

Для PHP отдельно: после json_encode сразу смотрите JSON_ERROR_UTF8. Невалидная байтовая строка из старой MySQL-таблицы в latin1 вылезает именно здесь. Лечится не «@json_encode», а приведением колонки и соединения к utf8mb4.

Кодировка, BOM, прокси

Отдавайте Content-Type: application/json; charset=utf-8 и реально пишите UTF-8. Редакторы вроде старого Блокнота на Windows дописывают BOM — снимите его. Nginx не должен подменять JSON HTML-страницей ошибки без того же статуса; если подменяет, клиент хотя бы увидит 502, а не «битый JSON с кодом 200».

Gzip и chunked encoding при обрыве дают обрезанный JSON: открыли объект, закрывающую скобку сеть съела. Парсер сообщает про неожиданный конец. Смотрите proxy_read_timeout, лимиты балансировщика и, для терминалов, физический контакт USB. Повторы идемпотентным ключом безопаснее, чем «нажать оплату ещё раз».

Среда Как разбирают Как видят ошибку Минимальная защита
Браузер / Node JSON.parse, Response.json() SyntaxError: Unexpected token Проверка status и Content-Type, try/catch с префиксом тела
PHP json_decode null + json_last_error() Сверять код ошибки, не считать любой null валидным
Python json.loads JSONDecodeError с pos Логировать doc.pos и doc.lineno, декодировать как UTF-8
Касса / POS JSON Драйвер банка + разбор учётки «Не вдалося розпарсити відповідь» Чистые реквизиты точки, актуальный драйвер, лог пакета

Сводка по средам — консенсус документации языков, не «народная примета». Позиция ошибки в Python и в V8 считается по-разному (символы против байт в некоторых ветках), поэтому в лог кладите и смещение, и живой фрагмент вокруг него.

Как не наступить на те же грабли завтра

Профилактика дешевле ночного дежурства. Контракт API фиксируйте: схема, кодировка, что происходит на 4xx/5xx, что возвращает пустой успех. JSON Schema или OpenAPI не бюрократия — это забор, через который не пролезает HTML логина.

На границе системы валидируйте вход. Не доверяйте «нам всегда приходит JSON». Терминал, партнёрский шлюз, старый мобильный клиент однажды пришлют текст. Логируйте сырой пакет при любом сбое разбора, режьте секреты, храните 24–72 часа. Без сырого тела разбор инцидента превращается в споры «у нас всё работает».

Для справочников, которые уезжают в JSON-терминал, введите запрет на » и управляющие символы ещё на форме ввода. Кассир не должен узнавать о кавычке в вывеске магазина в субботу в час пик. Банку проще сменить display-name точки заранее, чем ловить рваные пакеты на эквайринге.

Тесты пишите на плохих ответах, не только на счастливых. HTML 502, пустая строка, BOM, висячая запятая, кириллица в cp1251, обрезанный объект — пять фикстур, которые ловят регресс раньше клиента. Покрытие «успешный 200 с идеальным объектом» создаёт иллюзию надёжности.

Правило, которое стоит повесить над монитором: сначала смотрим байты ответа, потом чиним код. Пока вы не видели тело, вы чините гипотезу, а не ошибку.

Если после чистки реквизитов, обновления драйвера, проверки прокси и аккуратного разбора по статусу сообщение всё ещё вспыхивает на одном и том же запросе — сохраните пакет целиком. В нём почти всегда торчит конкретный символ: угловая скобка HTML, «умная» кавычка из Word, ноль байт после обрыва или двойная кавычка в названии точки. Парсер уже указал пальцем. Осталось перестать спорить с ним и посмотреть туда, куда он показывает.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *