Ответ 200, а внутри ошибка: как это ломает интеграции
Служба отвечает «успех», а в теле — отказ. Проверка по коду ответа считает такой ответ удачным, повтор не срабатывает, счётчик отказов показывает ноль. Инструмент разбирает ваш ответ и говорит, успех это или нет.
Потому что код ответа описывает доставку, а не результат. «200» значит,
что запрос дошёл, был понят и служба вернула тело. Что лежит в теле — код не сообщает.
Служба, отвечающая 200 с полем error внутри, для проверки
«status === 200» неотличима от удачной.
Наш случай: виджет, который врал о причине
08.08.2026 виджет на этом сайте получил от шлюза ответ с кодом 200 и пустым результатом.
Код обработки читал только result, не находил его и показывал посетителю
«обрыв генерации».
Это была выдумка. В ответе лежало поле error с настоящей причиной — она не
имела к обрыву никакого отношения. Одна строка в журнал с перечнем ключей ответа
показала: приходят error, status, log_id, а result
отсутствует вовсе.
Что именно сломалось
- Повтор не срабатывал. Логика повторов смотрела на код ответа. Отказ с кодом 200 повтора не вызывал — запрос просто терялся.
- Счётчик отказов показывал ноль. Мониторинг считал долю ответов с кодом ≥ 400. Отказы, приходящие с кодом 200, в неё не попадали, и график был идеально ровным при живой поломке.
-
Диагноз в журнале был ложным. Один
catchна все причины выдавал одно объяснение — то, которое написал разработчик, а не то, которое произошло. Искать по нему было бесполезно.
Минимальное воспроизведение
Возьмите свой клиент любой внешней службы и найдите место, где принимается решение
«удалось или нет». Если условие выглядит как if (response.ok) или
if (code === 200) и на этом заканчивается — воспроизведение готово,
искать больше нечего.
Проверить можно не ломая ничего: подставьте в разборщик ниже реальный ответ вашей службы в неудачном случае. Если он приходит с кодом 200 — вы уже в этой ситуации.
Разбор ответа
Вставьте код ответа и тело — скажем, успех это или отказ. Считает браузер: вставленное никуда не отправляется, чужие адреса не запрашиваются.
Что с этим делать в коде
- Решение «удалось или нет» принимайте по ТЕЛУ, а код используйте как первый фильтр. Порядок именно такой: сначала явные признаки отказа в теле, потом код.
- Ошибку читайте ПЕРВОЙ. Если сначала искать полезную нагрузку и не найти её, вы объявите причиной её отсутствие — и потеряете настоящую, которая лежала рядом.
- Пустой успех — не успех. Код 200, нет поля ошибки, но и нагрузки нет: показывать посетителю нечего, значит это отказ.
- В журнал — ключи ответа. Одна строка с перечнем полей находит причину быстрее, чем любое сообщение, придуманное заранее.
- Посетителю — честное «ответа нет». Называть причину, которой не знаешь, дороже, чем не называть: по ложному диагнозу ищут не там.
Где этот разбор неприменим
- Потоковые ответы (SSE, чанки) судить по одному телу нельзя: отказ приходит внутри потока, иногда в последнем событии.
- Службы со своим словарём. Поле может называться
ok: false,code: "FAILED"или вообще числом статуса внутри тела. Инструмент таких не знает — он ловит распространённые имена. - Частичный успех. Пакетные ручки возвращают массив, где часть элементов удалась, а часть нет. Общего вердикта у такого ответа нет вовсе, и любой инструмент, который его выдаёт, вводит в заблуждение.
Разбор подготовила QueryPort Engineering. Случай — наш собственный, 08.08.2026, на публичной ручке этого сайта. Нашли ошибку — напишите.
Связанные разборы
Если у вас счётчик отказов показывает ноль при живых жалобах пользователей — почти наверняка он считает по кодам ответа. Мы разбираем такие места на аудите: смотрим, где принимается решение «удалось», и что при этом попадает в мониторинг.