QueryPort

Ответ 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, на публичной ручке этого сайта. Нашли ошибку — напишите.

Связанные разборы

Если у вас счётчик отказов показывает ноль при живых жалобах пользователей — почти наверняка он считает по кодам ответа. Мы разбираем такие места на аудите: смотрим, где принимается решение «удалось», и что при этом попадает в мониторинг.

Как мы беремся за ИИ-интеграцию · Обсудить свою задачу