Files
progcode/editorial/agent-rewrites/012.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
16 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": 12,
"slug": "editorial-2027-09-practice-mentor-series",
"title": "Совместимый API-ответ: как поймать breaking change до релиза",
"excerpt": "Разбираем ответ HTTP-метода на границе сервиса: какие изменения ломают старого клиента, как отделить форму JSON от состояния системы и чем доказать совместимость.",
"contentHtml": "<p>Сервис возвращает HTTP 200, но клиент падает при разборе ответа. В логах появляется ошибка чтения поля, неизвестное значение перечисления или попытка вызвать метод у числа вместо строки. Сервер считает запрос успешным, а потребитель — нет. Цена ошибки растёт быстро: приходится искать все версии клиента, откатывать серверный код и решать, не потеряны ли уже записи, созданные по новой схеме.</p>\\n<p>Причина обычно не в синтаксической ошибке JSON. Команда меняет форму ответа как внутреннюю модель и не замечает потребителей. Она удаляет поле, делает новое поле обязательным, меняет тип или добавляет значение в enum. Каждый такой diff имеет собственный риск. Тезис простой: API-ответ нужно проверять как контракт двух сторон — по форме, по поведению старого клиента и по условиям, которые схема не описывает.</p>\\n<h2>Что именно обещает ответ</h2>\\n<p>Контракт начинается с конкретной границы: метод, путь, статус, media type и тело. Для <code>GET /customers/{id}</code> можно зафиксировать объект с обязательными полями <code>id</code>, <code>revision</code> и <code>state</code>. У <code>id</code> строковый тип. У <code>revision</code> положительное целое число. У <code>state</code> закрытый набор значений <code>active</code> и <code>blocked</code>.</p>\\n<p>Эта форма отвечает на вопрос «можно ли разобрать JSON». Она не отвечает на вопросы «имеет ли пользователь право видеть клиента» и «не устарела ли ревизия записи». Эти проверки относятся к авторизации и состоянию. Если смешать их со схемой, ответ об ошибке станет неточным: клиент не поймёт, нужно ли исправить запрос, обновить данные или прекратить повторные попытки.</p>\\n<pre><code>GET /customers/{id}\\nAccept: application/json\\n\\n200 OK\\nContent-Type: application/json\\n\\n{\\n &quot;id&quot;: &quot;customer-17&quot;,\\n &quot;revision&quot;: 4,\\n &quot;state&quot;: &quot;active&quot;\\n}</code></pre>\\n<p>Успешный ответ не становится совместимым только потому, что его принимает парсер JSON. Клиент может ветвить логику по <code>state</code>, строить URL из <code>id</code> и сравнивать <code>revision</code> с локальной версией. Сохранить синтаксис недостаточно: нужно сохранить значения и смысл, на которые опирается старый код.</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>Поле удалили или изменили его тип</td><td>Сравнить старую и новую схему и найти чтения поля</td><td>Сохранить поле или выпустить новую версию</td></tr><tr><td>Клиент попадает в ветку «неизвестное состояние»</td><td>Enum расширили без обработки нового значения</td><td>Прогнать старый switch на каждом значении</td><td>Добавить обработку либо не включать значение в старый контракт</td></tr><tr><td>Запросы начинают отклоняться после обновления</td><td>Новое поле объявили обязательным</td><td>Отправить старую форму без поля</td><td>Сделать поле необязательным или изменить версию</td></tr><tr><td>Клиент принимает ответ, но действует по неверной ветке</td><td>Сохранили тип, но изменили смысл значения</td><td>Проверить примеры поведения, а не только JSON Schema</td><td>Сохранить семантику или переименовать поле</td></tr><tr><td>Ответ формально верен, но операция получает отказ</td><td>Нарушено право или текущее состояние ресурса</td><td>Проверить авторизацию и условие версии отдельно</td><td>Вернуть точный 403/409 и не маскировать его под 400</td></tr></tbody></table></div>\\n<p>Добавление необязательного поля чаще всего совместимо: старый клиент его игнорирует. Но это правило действует только для потребителя, который действительно игнорирует неизвестные свойства. У строгого декодера или схемы с запретом дополнительных полей появится отказ. Поэтому решение принимают по реальным правилам клиента, а не по названию изменения.</p>\\n<h2>Учебный валидатор на границе</h2>\\n<p>Следующая функция показывает минимальную проверку ответа. Она не ходит в сеть и не читает базу. Входом служит уже разобранный JavaScript-объект. Функция принимает только известную форму и возвращает нормализованное значение. Это учебный пример: он показывает границу контракта, но не заменяет OpenAPI, JSON Schema, интеграционный тест или авторизацию.</p>\\n<pre><code>function validateCustomerResponse(payload) {\\n if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {\\n return { ok: false, reason: 'body-must-be-object' };\\n }\\n\\n if (typeof payload.id !== 'string' || payload.id.length === 0) {\\n return { ok: false, reason: 'id-must-be-non-empty-string' };\\n }\\n\\n if (!Number.isInteger(payload.revision) || payload.revision &lt; 1) {\\n return { ok: false, reason: 'revision-must-be-positive-integer' };\\n }\\n\\n if (!['active', 'blocked'].includes(payload.state)) {\\n return { ok: false, reason: 'state-is-outside-enum' };\\n }\\n\\n return {\\n ok: true,\\n value: { id: payload.id, revision: payload.revision, state: payload.state },\\n };\\n}\\n\\nconst accepted = validateCustomerResponse({\\n id: 'customer-17', revision: 4, state: 'active',\\n});\\nconst rejected = validateCustomerResponse({\\n id: 'customer-17', revision: 4, state: 'deleted',\\n});\\n\\nconsole.log(accepted.ok, accepted.value.state);\\nconsole.log(rejected.ok, rejected.reason);\\n// true active\\n// false state-is-outside-enum</code></pre>\\n<p>Отдельный отрицательный пример важнее ещё одного успешного fixture. Если сервер начнёт отправлять <code>state: deleted</code>, валидатор обнаружит изменение до того, как клиент выполнит неверную ветку. Если сервер отправит <code>revision: &quot;4&quot;</code>, отказ произойдёт по типу. Если поле исчезнет, причина должна назвать поле, а не скрыться за общим сообщением <code>invalid response</code>.</p>\\n<figure><img src=\"/assets/editorial/2027/mentor-series-2027-skill-map-contract.svg\" alt=\"Схема проверки API-контракта: ответ проходит проверку формы, затем проверку потребителя и только после этого используется клиентом.\" loading=\"lazy\" /><figcaption>Граница совместимости состоит из трёх проверок: форма ответа, поведение потребителя и условия операции. Схема не доказывает наличие права доступа или актуальность данных.</figcaption></figure>\\n<h2>Порядок проверки перед изменением</h2>\\n<ol><li>Назовите endpoint, метод, статус и media type. Отделите тело ответа от заголовков, запроса и внутренней модели.</li><li>Снимите текущую форму: обязательные поля, типы, nullable, enum и значения по умолчанию. Сохраните один успешный и несколько отрицательных примеров.</li><li>Найдите потребителей. Проверьте чтение полей, ветвления по enum, строгие декодеры и преобразователи DTO. Один найденный клиент не доказывает, что найден каждый.</li><li>Сравните старую и новую форму. Отдельно отметьте удаление поля, изменение типа, сужение enum и появление обязательного свойства.</li><li>Запустите runtime-валидатор на старом и новом ответе. Ошибка должна указывать путь к полю и причину отказа.</li><li>Прогоните consumer contract test со старым клиентом. Проверяйте не только десериализацию, но и ветку поведения для каждого допустимого значения.</li><li>Проверьте отрицательный путь: неизвестное поле, пропущенное поле, неверный тип, неизвестное enum-значение, 403 и конфликт версии. Для каждого случая зафиксируйте ожидаемый статус.</li><li>Если изменение несовместимо, выберите действие: сохранить старое поле, добавить новое рядом, открыть период deprecated или выпустить новую версию. Запишите условие удаления.</li></ol>\\n<h2>Где заканчивается JSON Schema</h2>\\n<p>Схема хорошо описывает типы, обязательность и ограничения документа. Она может запретить лишние поля или определить ветвление по значению. Но она не видит пользователя, базу и время. Ответ <code>state: active</code> может быть синтаксически правильным, хотя запись уже заблокирована. Значение <code>revision: 4</code> не доказывает, что обновление с ревизией 3 ещё допустимо.</p>\\n<p>Состояние требует отдельного протокола. Для конкурентного обновления подойдут версия ресурса и условный запрос с <code>If-Match</code>; для права — проверка роли до изменения; для отсутствующего ресурса — договорённый статус 404. Не превращайте 409 в 400: клиенту нужен сигнал, что запрос сформирован правильно, но состояние изменилось. Не повторяйте 403 автоматически: повтор не добавит прав.</p>\\n<p>Есть и отрицательный путь для самой проверки. Схема может пройти, а реальный handler — вернуть другой ответ в исключении. Поэтому contract test должен вызвать маршрут, проверить статус, заголовок и тело. Runtime-проверка должна работать на фактическом ответе, а не только на вручную собранном объекте. Это снижает конкретный риск, но не доказывает, что список потребителей полон.</p>\\n<h2>Ограничения и критерий готовности</h2>\\n<p>Учебная функция не проверяет OpenAPI-документ, сериализацию фреймворка, авторизацию, транзакции, сетевые сбои и содержимое базы. Она также не решает вопрос обратной совместимости для неизвестного клиента. Эти границы нужно оставить видимыми, иначе локальный зелёный тест создаст ложное чувство безопасности.</p>\\n<p>Изменение готово к выпуску, если команда может показать четыре доказательства: новая форма проходит schema- и runtime-проверку; старый клиент проходит consumer contract test; отрицательные случаи возвращают согласованные статусы и причины; для breaking change указаны версия, период совместимости и проверяемое условие удаления. Если хотя бы одного доказательства нет, результат следует считать непроверенным, даже когда сборка завершилась успешно.</p>\\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://spec.openapis.org/oas/v3.2.0.html\" target=\"_blank\" rel=\"noopener noreferrer\">OpenAPI Specification 3.2.0</a> — официальный формат описания HTTP-интерфейсов и операций.</li><li><a href=\"https://json-schema.org/draft/2020-12/json-schema-core.html\" target=\"_blank\" rel=\"noopener noreferrer\">JSON Schema Core 2020-12</a> — официальная спецификация языка схем для JSON-документов.</li><li><a href=\"https://www.rfc-editor.org/rfc/rfc9110.html\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 9110: HTTP Semantics</a> — стандарт IETF для методов, статусов и семантики HTTP.</li></ul>"
}