Files
progcode/editorial/agent-rewrites/048.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
18 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 48,
"slug": "editorial-2026-09-practice-frontend-backend-boundary",
"title": "Ответ 200 не описывает экран: как проверить модель на границе UI и API",
"excerpt": "Клиент может получить успешный HTTP-ответ и всё равно показать неверное состояние. Разбираем порядок проверок, runtime-контракт и отрицательные пути для screen model.",
"contentHtml": "<p>Кнопка показывает «Готово». Запрос завершился с кодом 200. Пользователь нажимает «Изменить», а компонент падает: поле переименовали, список действий пришёл строкой или новый статус не попал в клиентский код. Иногда экран не падает. Он выбирает состояние по умолчанию и показывает устаревшую информацию.</p>\n<p>Цена ошибки выше исключения. Пользователь повторяет команду. Оператор ищет проблему в сети. Разработчик смотрит на типы, которые были верны во время сборки. На границе процесса уже лежит другой JSON. Если UI записал его в state без проверки, ошибка проявится только в редком сценарии.</p>\n<p>Тезис статьи прост: статус HTTP сообщает результат обмена, но не доказывает, что тело подходит конкретному экрану. Клиент должен проверить HTTP, определить наличие representation, разобрать JSON, проверить минимальную screen model и только потом передать данные компоненту. Для каждого шага нужен отрицательный путь.</p>\n<h2>Что именно считается успехом</h2>\n<p>У ответа есть несколько уровней смысла. Код 200 сообщает, что сервер обработал запрос успешно на уровне операции. Заголовок <code>Content-Type</code> заявляет формат тела. JSON parser проверяет синтаксис. Runtime-валидатор проверяет поля, которые нужны экрану. Ни один предыдущий шаг не заменяет следующий.</p>\n<p>Тело <code>{\"status\":\"done\",\"allowedActions\":\"edit\"}</code> может быть корректным JSON. Но экран, который знает только <code>ready</code>, <code>pending</code> и <code>blocked</code>, не может безопасно выбрать состояние для <code>done</code>. Строка вместо массива также не становится моделью от того, что в ней записано знакомое слово.</p>\n<p>Граница принадлежит адаптеру данных, а не JSX-компоненту. Компонент получает проверенную модель или явный результат ошибки. Он не должен угадывать неизвестный статус, подставлять пустой массив и считать это подтверждённым состоянием.</p>\n<figure><img src=\"/assets/editorial/2026/frontend-backend-boundary-2026-state-ownership-map.svg\" alt=\"Поток проверки ответа: HTTP status, Content-Type, JSON parse, screen model и рендер; ошибка останавливает поток до компонента.\" loading=\"lazy\" /><figcaption>Проверки идут от транспорта к экрану. Ошибка на любом шаге останавливает передачу данных в render state.</figcaption></figure>\n<h2>Минимальная модель экрана</h2>\n<p>Не копируйте в UI всю доменную сущность. Выпишите поля, по которым компонент принимает решение. Для экрана заказа это состояние, набор известных действий и код сообщения. У каждого поля есть тип, допустимые значения и действие при нарушении.</p>\n<div class=\"table-scroll\"><table><caption>Минимальная screen model для состояния заказа</caption><thead><tr><th scope=\"col\">Поле</th><th scope=\"col\">Допустимое значение</th><th scope=\"col\">Решение UI</th><th scope=\"col\">Отрицательный путь</th></tr></thead><tbody><tr><td><code>status</code></td><td><code>ready | pending | blocked</code></td><td>выбрать состояние экрана</td><td>остановить render</td></tr><tr><td><code>allowedActions</code></td><td>массив известных команд</td><td>показать разрешённые кнопки</td><td>не создавать кнопку</td></tr><tr><td><code>messageCode</code></td><td>непустая строка</td><td>выбрать локализованный текст</td><td>показать безопасную ошибку</td></tr><tr><td>HTTP status</td><td>статус операции</td><td>отделить успех от отказа</td><td>не строить модель только по числу</td></tr></tbody></table></div>\n<p>Словарь статусов должен быть закрытым, пока команда не описала новый переход. Это не запрет на развитие API. Новый статус должен менять контракт, обработчик и тест. Молчаливый fallback скрывает изменение API и превращает его в случайный UI-дефект.</p>\n<h2>Рабочий пример: проверяем ответ до render</h2>\n<p>Ниже учебный пример на JavaScript. Списки статусов и действий выбраны для демонстрации. Они не описывают production API и не обещают production-результат. В реальном проекте их заменяет контракт конкретного endpoint.</p>\n<pre><code>const statuses = new Set(['ready', 'pending', 'blocked']);\nconst actions = new Set(['retry', 'edit', 'cancel']);\nfunction validateScreenModel(value) {\n const errors = [];\n if (!value || typeof value !== 'object' || Array.isArray(value)) return { valid: false, errors: ['body-must-be-object'] };\n if (!statuses.has(value.status)) errors.push('status-is-unknown');\n if (!Array.isArray(value.allowedActions)) errors.push('allowedActions-must-be-array');\n else if (value.allowedActions.some((item) =&gt; !actions.has(item))) errors.push('allowedActions-contains-unknown-action');\n if (typeof value.messageCode !== 'string' || value.messageCode.length === 0) errors.push('messageCode-must-be-non-empty');\n return { valid: errors.length === 0, errors };\n}\nasync function readScreen(response) {\n if (response.status === 204) return { kind: 'empty-success', next: 'refetch-screen' };\n if (!response.ok) return { kind: 'http-error', status: response.status };\n if (!response.headers.get('content-type')?.includes('application/json')) return { kind: 'wrong-media-type' };\n let body;\n try { body = await response.json(); } catch { return { kind: 'invalid-json' }; }\n const result = validateScreenModel(body);\n return result.valid ? { kind: 'screen-model-ready', model: body } : { kind: 'invalid-screen-model', errors: result.errors };\n}</code></pre>\n<p>Функция возвращает классификацию, а не случайную строку из parser. Такой результат связывается с error boundary, логом и безопасным состоянием компонента. В технический канал передавайте код нарушения и request id. Не отправляйте весь payload: он может содержать персональные или доменные данные.</p>\n<p>Порядок проверок важен. Для 204 нельзя вызывать <code>response.json()</code>: успешное выполнение не означает наличие representation. Для неверного <code>Content-Type</code> повтор запроса обычно не исправит формат. Для невалидной модели retry тоже не является решением: сервер может стабильно возвращать тот же payload.</p>\n<h2>Симптомы и действия на границе</h2>\n<div class=\"table-scroll\"><table><caption>Симптом → причина → проверка → действие</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Экран показал «Готово», следующий клик падает</td><td>200 приняли за модель</td><td>сравнить тело с runtime-схемой</td><td>запретить запись в state до валидации</td></tr><tr><td>DELETE вызывает ошибку parser</td><td>helper ждёт JSON после 204</td><td>проверить статус до <code>json()</code></td><td>вернуть empty-success и перечитать ресурс</td></tr><tr><td>Появилась кнопка неизвестной команды</td><td>UI доверяет строке action</td><td>проверить каждый элемент словарём</td><td>отклонить модель и не создавать кнопку</td></tr><tr><td>422 превратился в общий toast</td><td>прикладной отказ назвали сетью</td><td>прочитать machine code и поле</td><td>подсветить поле, если контракт это разрешает</td></tr><tr><td>После timeout команда выполнилась дважды</td><td>клиент повторил запрос вслепую</td><td>проверить idempotency key и статус операции</td><td>не повторять; перечитать результат</td></tr><tr><td>Типы проходят, внешний сервис прислал другой JSON</td><td>тип сборки не проверяет сеть</td><td>воспроизвести фактический ответ</td><td>добавить contract или runtime test</td></tr></tbody></table></div>\n<h2>Query, command и пустой результат</h2>\n<p>Чтение и изменение состояния требуют разных правил. Query получает representation и обновляет экран после проверки. Command просит сервер изменить состояние. Ответ 202 означает принятие асинхронной работы, а не завершённый переход. Ответ 204 означает успешную операцию без тела. В обоих случаях клиенту нужен путь к актуальной модели.</p>\n<p>Не превращайте ответ команды в локальный флаг <code>isSuccess</code>. Если запрос оборвался после отправки, клиент не знает, успел ли сервер изменить ресурс. Повтор POST может создать вторую операцию. Безопасный вариант зависит от API: idempotency key, endpoint статуса или повторное чтение. Если контракт ничего не даёт, UI не может честно обещать результат после timeout.</p>\n<p>Ошибки 409, 422, 429 и 5xx нельзя свести к одному toast. 409 может требовать перечитать конфликтующий ресурс. 422 может вернуть ошибки полей. 429 может содержать ограничение частоты и время следующей попытки. 5xx допускает повтор только при известной идемпотентности и ограниченном бюджете. Код статуса сужает выбор, но не заменяет error detail и правило следующего действия.</p>\n<h2>Как внедрить границу</h2>\n<ol><li>Выберите endpoint, где компонент читает поля напрямую или использует fallback после ошибки parser.</li><li>Зафиксируйте intent: query, command или получение результата фоновой операции.</li><li>Выпишите минимальную screen model и закройте словари статусов, действий и кодов сообщений.</li><li>Разделите обработку HTTP, media type, JSON parse и runtime validation. Для 204 задайте результат без тела.</li><li>Определите отрицательный путь для неизвестного поля, неверного типа, 4xx, 5xx и timeout.</li><li>Добавьте тесты на валидный ответ, неизвестный статус, неправильный массив, пустое тело и неверный media type.</li><li>Передавайте в компонент только проверенную модель. Ошибку показывайте безопасным состоянием, диагностику связывайте с request id.</li><li>Проверьте повтор команды отдельно и зафиксируйте, как клиент узнаёт результат после обрыва сети.</li></ol>\n<h2>Ограничения</h2>\n<p>Runtime-валидатор проверяет форму данных, но не доказывает бизнес-истину. <code>status: ready</code> может быть формально корректным и устареть через секунду. Для этого нужны версия ресурса, контроль конкуренции, кэш-политика или повторное чтение. Валидация не заменяет authorization: наличие действия в JSON не выдаёт право на серверную операцию.</p>\n<p>OpenAPI и сгенерированные TypeScript-типы описывают договорённость, но тип на этапе сборки не проверяет байты из сети. При независимом выпуске сервисов оставьте тест фактического ответа или runtime-схему. Ручная функция подходит для маленькой модели. Для сложных вложенных структур используйте schema validator и измерьте стоимость на реальном размере payload.</p>\n<p>Учебный пример не описывает конкретную бизнес-модель. Он показывает место проверки и решения, которые нужно подтвердить контрактом проекта. Если один endpoint обслуживает несколько экранов, не расширяйте универсальный объект бесконечно. Назовите отдельные read model или версионируйте ответ.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Граница готова, если для каждого поддержанного ответа команда может назвать четыре вещи: какую модель можно передать в UI, какое действие доступно, какое запрещено и каким тестом это доказано. Тест должен показать, что валидный 200 проходит в render, 204 не запускает JSON parser, неизвестный статус останавливает render, а timeout команды не вызывает слепой повтор.</p>\n<p>Проверка незавершена, если компонент выбирает состояние по умолчанию после ошибки схемы или строит кнопку из свободной строки. Искомый результат — наблюдаемая граница, где транспортный ответ превращается в screen model только после проверки.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://www.rfc-editor.org/rfc/rfc9110.html\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 9110: HTTP Semantics</a> — официальная спецификация семантики методов, представлений и статус-кодов HTTP.</li><li><a href=\"https://www.rfc-editor.org/rfc/rfc9457.html\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 9457: Problem Details for HTTP APIs</a> — официальный формат машинно-читаемых деталей ошибки для HTTP API.</li><li><a href=\"https://spec.openapis.org/oas/latest.html\" target=\"_blank\" rel=\"noopener noreferrer\">OpenAPI Specification</a> — официальное описание операций HTTP API и вариантов ответов.</li></ul>"
}