Симптом «кнопка показывает одно, API — другое» редко объясняется одной строкой. В браузере мог остаться старый store, прокси мог вернуть не тот Content-Type, команда могла завершиться 409, а компонент — считать любой ответ подтверждением. Цена бессистемной диагностики — повторные ручные проверки и исправление не той стороны: команда меняет сервер, когда проблема в render input, или переписывает UI, когда контракт уже нарушен.
Нужен короткий протокол, который фиксирует наблюдаемые факты в правильном порядке: пользовательское намерение, запрос, ответ и фактический вход в рендер. Нельзя начинать с гипотезы «кэш виноват» или «backend сломался». В статье соберём безопасную карточку расследования, дадим чистую функцию для сравнения данных и покажем, какие выводы разрешены на каждом шаге.
Четыре снимка одной операции
Первый снимок — намерение: какой action выбрал пользователь и с какими нормализованными параметрами. Не нужно сохранять весь ввод; достаточно request id, operation name и безопасного класса входа. Второй — исходящий HTTP: method, route template, статус и заголовок correlation/request id без query-параметров и секретов. Третий — ответ: status, Content-Type, размер и проверенная форма тела. Четвёртый — объект, который реально получил компонент после adapter.
| Факт | Минимальное поле | Безопасный пример | Какой вопрос закрывает |
|---|---|---|---|
| Intent | operation + input class | update-address / valid-form | какую команду хотел выполнить UI? |
| Request | method + route template + request id | PATCH /orders/:id + req-42 | что действительно ушло за границу? |
| Response | status + media type + contract result | 409 + problem + valid envelope | что сервер сообщил на уровне контракта? |
| Render input | view model + revision | blocked + version 18 | что именно получил компонент? |
| Decision | next action | refetch / fix field / inspect adapter | какой следующий тест даст различие? |
Сначала сравнить, потом объяснять
Если response valid, а render input старый, ищите adapter, cache key или race между двумя запросами. Если render input совпадает с ответом, но на экране другое, проверяйте selector, memoization и локальный optimistic state. Если response invalid, UI не обязан его отображать; источник проблемы находится на контрактной границе. Если status 409, не называйте его «ошибкой сети»: запрос дошёл, но переход не состоялся по прикладной причине.
Полезно сохранять revision или version, если API их предоставляет. Timestamp не заменяет версию: часы разных процессов могут отличаться, а более поздний ответ не всегда относится к более новому состоянию. Сравнение версии помогает обнаружить race: запрос A ушёл первым, B — вторым, но B вернулся раньше. Без явного правила store может принять старый ответ последним.
Проверяемый пример сравнения
import { diagnoseBoundaryObservation } from './upgrade-2026-09.mjs';
console.log(diagnoseBoundaryObservation({
response: {
status: 202,
contentType: 'application/json',
body: { status: 'blocked', allowedActions: ['retry'], messageCode: 'order.sync_required' },
},
ui: { renderedStatus: 'ready' },
}));
// { status: 'ui-api-state-mismatch', next: 'compare-render-input-with-response-body' }Эта функция не читает браузер и не делает сетевой запрос. Она полезна как тест для диагностического слоя: при одинаковом контракте она отличает расхождение render input от ошибки формы. В приложении фактические снимки нужно получать из инструментов с фильтрацией персональных данных. Не записывайте body целиком в лог только потому, что так проще: request id и hash безопасного класса часто достаточно, чтобы связать события.
При отсутствии response функция возвращает отдельное состояние, а не «UI stale». Это важная дисциплина. Нельзя приписывать причину месту, которое ещё не наблюдали. Так же нельзя считать отсутствие нового render proof того, что backend не ответил: событие могло потеряться в adapter, отмениться при unmount или быть отброшено как устаревшее.
Сигналы cache и race
Cache обычно выдаёт повторяемость: один и тот же ключ возвращает прежнюю версию, хотя network request уже получил новую. Race выдаёт зависимость от порядка: обновление состояния меняется при задержке одного из ответов. Для проверки cache сравните request key, revision и источник данных. Для race искусственно задержите только тестовый ответ и проверьте правило принятия версии. Не делайте вывод о поведении пользователя по одному снимку.
Если команда использует optimistic UI, назовите два состояния: локальное подтверждение взаимодействия и подтверждённая read model. Пока ответ не прочитан, на кнопке допустимо показать spinner или disable, но нельзя менять общий status на «ready» только потому, что click handler завершился. После ошибки optimistic patch должен быть снят или помечен как требующий решения. Иначе следующий render унаследует ложную модель.
HTTP-инструменты и граница их показаний
DevTools Network показывает запрос и ответ в конкретном браузере. curl позволяет повторить HTTP-обмен, но не воспроизводит selector, cookie policy или race компонента. Логи BFF показывают серверную обработку, но не доказывают, что браузер получил именно этот response. Каждый инструмент закрывает свою границу. Скриншот одного слоя нельзя использовать как доказательство другого.
Для curl-проверки сохраняйте только безопасные заголовки и заменяйте реальные идентификаторы тестовыми. Проверяйте статус и Content-Type, затем тело на соответствие schema. Если endpoint требует авторизацию, используйте тестовый токен с ограниченным сроком; не вставляйте секрет в статью, shell history или issue. В production не повторяйте команду изменения без знания идемпотентности.
Пример минимальной curl-проверки
curl --fail-with-body --silent --show-error \
-H 'Accept: application/json' \
-H 'X-Request-Id: req-42' \
'https://api.example.test/orders/42' \
| jq '{status, allowedActions, messageCode}'Команда подходит для чтения тестового ресурса, а не для бездумного повторения mutation. --fail-with-body не валидирует screen model и не превращает ошибку в успех; jq только выбирает поля для просмотра. В рабочем расследовании замените host, путь и идентификатор на разрешённые тестовые значения и сохраните рядом HTTP-статус и Content-Type. Если ответ не JSON, это отдельная находка, а не повод считать jq виноватым.
Порядок расследования
- Зафиксировать operation name, request id и класс входа без персональных значений.
- Снять method, route template, HTTP status и Content-Type; не повторять mutation до проверки идемпотентности.
- Проверить response по контракту и отделить transport failure от problem detail и domain rejection.
- Сравнить проверенный response с render input: version, status, allowed actions и message code.
- Если они расходятся, проверить adapter, cache key, optimistic patch и порядок ответов.
- Сформулировать один следующий тест, который различает две оставшиеся гипотезы, и только затем менять код.
Когда остановить расследование
Остановитесь, если для следующего вывода нужны неполученные данные: реальный body с персональными полями, доступ к production-логам или повторная команда с неизвестным эффектом. Это не бюрократия, а граница доказательства. Попросите владельца системы дать безопасный correlation id, redacted response или тестовый reproduction. Нельзя заполнять пробел правдоподобной историей о кэше или сервере.
Если четыре снимка согласованы, а пользователь всё ещё видит другое, расследование переходит в слой представления: selector, memoization, hydration или CSS. Если несогласован только response, проблема остаётся у producer или gateway. Если request отличается от intent, ищите mapping формы. Такая классификация сокращает область поиска и делает исправление проверяемым.
Ограничения и следующий шаг
Протокол не заменяет distributed tracing, contract testing и security review. Он не сообщает, что данные можно хранить сколько угодно, и не разрешает логировать тело ответа. Его задача уже: разделить наблюдения по границам и не назвать гипотезу фактом. Для сложной асинхронной команды добавьте message id, version и отдельный статус обработки; не растягивайте одну HTTP-карточку на очередь.
Следующий шаг — сделать один интеграционный тест, который сохраняет четыре безопасных поля и воспроизводит stale render input. Затем добавьте regression test на неверный Content-Type и test на более старую version. Когда эти тесты проходят, команда получает не красивый отчёт, а короткий маршрут от симптома к конкретной границе.
Проверяемые источники
- RFC 9110: HTTP Semantics — RFC 9110, июнь 2022. Использован для различения ответа HTTP, representation и прикладной семантики статусов. Граница: Не показывает состояние конкретного браузера или store.
- WHATWG Fetch Standard — живой стандарт WHATWG, разделы Fetch и response handling. Использован для границы между Fetch response и решением приложения об обработке body. Граница: Стандарт не задаёт ваш adapter, cache policy или UI render.
- RFC 9457: Problem Details for HTTP APIs — RFC 9457, июль 2023. Использован для структурированного problem envelope при диагностике 4xx/5xx. Граница: Не является логом конкретного сервиса и не заменяет redaction policy.