Files
progcode/editorial/agent-rewrites/012.json
T

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": 12,
"slug": "editorial-2027-09-practice-mentor-series",
"title": "Совместимый API-ответ: как поймать breaking change до релиза",
"excerpt": "Разбираем ответ HTTP-метода на границе сервиса: какие изменения ломают старого клиента, как отделить форму JSON от состояния системы и чем доказать совместимость.",
"contentHtml": "<p>Сервис возвращает HTTP 200, но клиент падает при разборе ответа. В логах видны неизвестное значение enum, отсутствие поля или вызов метода у числа вместо строки. Сервер считает запрос успешным, а потребитель — нет. Цена ошибки — поиск версии клиента, срочный откат и проверка кэшей или данных, если изменение затронуло их формат.</p>\n<p>Проблема возникает, когда форму ответа считают внутренней деталью. Удаление свойства, изменение типа, добавление обязательного поля и новое значение enum меняют контракт по-разному. Ниже — способ проверить один HTTP-ответ до релиза: сначала зафиксировать границу, затем прогнать старого потребителя и только после этого выбирать совместимое расширение или новую версию.</p>\n<h2>Контракт начинается с границы</h2>\n<p>В учебном примере граница — <code>GET /customers/{id}</code>, статус <code>200</code>, media type <code>application/json</code> и тело ответа. Направление тоже входит в контракт: request отправляет клиент, response читает клиент. Поэтому обязательное поле в запросе и обязательное поле в ответе нельзя оценивать одним правилом.</p>\n<p>OpenAPI описывает HTTP-операцию, её ответы и доступную потребителю форму интерфейса. JSON Schema проверяет экземпляр JSON по типам, обязательным полям и ограничениям. Эти инструменты отвечают на разные части вопроса. Ни один из них сам по себе не доказывает, что фактический handler отдаёт описанное тело, что у пользователя есть право на ресурс или что ревизия записи ещё актуальна.</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>HTTP 200 подтверждает успешную обработку запроса на уровне протокола, но не совместимость представления со старым кодом. Потребитель может ветвить логику по <code>state</code>, строить URL из <code>id</code> и сравнивать <code>revision</code> с локальной версией. Синтаксически правильный JSON всё равно ломает клиент, если изменились тип, допустимые значения или смысл поля.</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><code>response</code>: поле удалили или переименовали</td><td>Старый клиент обращается к property</td><td>Найти чтения поля и старые fixtures</td><td>Сохранить поле на deprecated-период или выпустить версию</td></tr><tr><td><code>response</code>: изменили тип или смысл</td><td>Десериализатор или бизнес-ветка принимает неверное значение</td><td>Проверить тип и поведение старого клиента</td><td>Добавить новое поле с новым именем или сохранить семантику</td></tr><tr><td><code>response</code>: добавили значение enum</td><td>Строгий decoder или ветка по умолчанию не знает значение</td><td>Прогнать старый код на каждом допустимом значении</td><td>Не включать значение в старый контракт или подготовить новую версию</td></tr><tr><td><code>request</code>: добавили required-поле</td><td>Старый отправитель получает отказ</td><td>Отправить новую форму без поля</td><td>Сделать поле optional, дать default или изменить версию</td></tr><tr><td><code>response</code>: добавили optional-поле</td><td>Обычно ничего, но strict decoder может отклонить неизвестный ключ</td><td>Проверить реальную политику неизвестных полей</td><td>Зафиксировать поведение decoder и добавить contract-test</td></tr></tbody></table></div>\n<p>Слово <code>breaking</code> относится не к строке diff, а к конкретному потребителю и направлению обмена. Новое поле в response обычно расширяет контракт, если старый decoder игнорирует неизвестные ключи. Но схема с <code>additionalProperties: false</code> или строгая библиотека могут сделать такое расширение несовместимым. Решение принимают по исполняемому правилу клиента, а не по названию изменения.</p>\n<h2>Учебный валидатор ответа</h2>\n<p>Функция ниже получает уже разобранный JavaScript-объект. Она не ходит в сеть, не читает базу и не проверяет право доступа. Валидатор извлекает известные поля и возвращает ясную причину отказа. Дополнительные ключи он не использует и не объявляет допустимыми: политику strict или permissive нужно задать отдельной схемой и тестом.</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.trim() === '') {\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 unknownState = validateCustomerResponse({\n id: 'customer-17', revision: 4, state: 'deleted',\n});\nconst wrongRevision = validateCustomerResponse({\n id: 'customer-17', revision: '4', state: 'active',\n});\n\nconsole.log(accepted.ok, accepted.value.state);\n// true active\nconsole.log(unknownState.ok, unknownState.reason);\n// false state-is-outside-enum\nconsole.log(wrongRevision.ok, wrongRevision.reason);\n// false revision-must-be-positive-integer</code></pre>\n<p>Отрицательные случаи показывают, где остановился контракт. <code>state: deleted</code> не должен тихо попасть в ветку для активного клиента, а строка <code>&quot;4&quot;</code> не должна превратиться в число без явного правила. Если обязательное поле исчезло, причина должна назвать его. Такой адаптер полезен на границе, но его зелёный результат не заменяет вызов реального HTTP-маршрута.</p>\n<figure><img src=\"/assets/editorial/2027/mentor-series-2027-skill-map-contract.svg\" alt=\"Схема проверки HTTP-ответа: endpoint передаёт JSON в проверку формы, затем совместимый объект клиенту; неверный тип или enum ведёт к отказу.\" loading=\"lazy\" /><figcaption>Сначала проверяется форма ответа, затем клиент получает нормализованный объект. Красная ветка показывает отказ на неверном типе или значении; схема не заменяет проверку прав и состояния ресурса.</figcaption></figure>\n<h2>Проверка до релиза</h2>\n<ol><li>Зафиксируйте старый и новый контракт отдельно для request и response: метод, путь, статус, media type, обязательные поля, типы, nullable и enum.</li><li>Соберите fixtures из фактического HTTP-ответа. Сохраните успешный случай, пропущенное поле, неверный тип и неизвестное enum-значение, а не только вручную созданный объект.</li><li>Проверьте форму через OpenAPI или JSON Schema, затем прогоните runtime-проверку на ответе реального handler. Сверьте статус, заголовок <code>Content-Type</code> и тело.</li><li>Найдите всех известных потребителей: чтения property, строгие decoders, DTO-мэпперы, сгенерированные SDK, кэши и события. Один найденный клиент не доказывает полноту списка.</li><li>Запустите старую версию клиента против нового ответа. Для enum проверьте каждое значение, для optional-полей — поведение при неизвестном ключе, а для изменения типа — реальную десериализацию.</li><li>Разведите ожидаемые ошибки. Зафиксируйте, что означает <code>400</code> для неверной формы, <code>403</code> для отказа в праве, <code>404</code> для отсутствующего ресурса, <code>409</code> для конфликта состояния и <code>412</code> для невыполненного условия <code>If-Match</code>.</li><li>Выберите обратимый ход: сохранить старое поле, добавить новое рядом, открыть окно deprecated или выпустить новую версию. Для удаления запишите срок и наблюдаемый сигнал использования.</li><li>После rollout сравните ошибки старого и нового клиентов, а затем удаляйте старую форму только после проверки этого сигнала. Не считайте зелёную сборку доказательством неизвестных внешних потребителей.</li></ol>\n<h2>Схема не видит состояние</h2>\n<p>JSON Schema хорошо описывает документ: типы, обязательность, enum и дополнительные свойства. Она не видит пользователя, базу и время. Ответ с <code>state: active</code> может соответствовать схеме, хотя запись уже заблокирована. <code>revision: 4</code> не доказывает, что обновление поверх ревизии 3 ещё разрешено.</p>\n<p>Состояние и право требуют отдельного протокола. Для авторизации сервис проверяет роль и возвращает согласованный отказ. Для конкурентного обновления он может использовать версию ресурса и условный запрос с <code>If-Match</code>; если условие не выполнено, клиенту нужен отдельный сигнал <code>412 Precondition Failed</code>. Конфликт доменного состояния может быть <code>409 Conflict</code>. Не маскируйте эти случаи под <code>400</code>: форма запроса может быть правильной, а причина отказа — в праве или текущем состоянии.</p>\n<p>Есть и отрицательный путь для самой проверки. Схема может пройти, а реальный handler — вернуть другой ответ в исключении или на редком кодовом пути. Поэтому contract-test должен вызвать маршрут и проверить фактические статус, заголовок и тело. Runtime-проверка снижает риск несовместимого payload, но не доказывает, что список потребителей полон.</p>\n<h2>Ограничения и критерий готовности</h2>\n<p>Учебный валидатор не проверяет OpenAPI-документ, сериализацию фреймворка, авторизацию, транзакции, сетевые сбои, кэш и содержимое базы. Он также не решает вопрос обратной совместимости для неизвестного клиента. Эти границы нужно оставить видимыми, иначе локальный зелёный тест создаст ложное чувство безопасности.</p>\n<p>Изменение готово к выпуску, когда команда может показать четыре доказательства:</p>\n<ul><li>новая форма проходит schema- и runtime-проверку на фактическом ответе;</li><li>старый клиент проходит consumer contract test и обрабатывает каждое допустимое значение;</li><li>отрицательные случаи возвращают согласованные статусы и причины;</li><li>для breaking change указаны версия, период совместимости и проверяемое условие удаления.</li></ul>\n<p>Если одного доказательства нет, результат следует считать непроверенным, даже когда сборка завершилась успешно. Возьмите один настоящий endpoint, заведите для него положительный и отрицательные fixtures, а затем повторите проверку после изменения схемы. Такой маленький контур быстрее полного аудита и оставляет след, который можно повторить в CI.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://spec.openapis.org/oas/v3.2.0.html\" target=\"_blank\" rel=\"noopener noreferrer\">OpenAPI Specification v3.2.0</a> — официальный формат описания HTTP-интерфейсов, операций и ответов; спецификация не доказывает, что runtime возвращает заявленное тело.</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 для методов, статусов, представлений и условных запросов; он не задаёт формат ошибки конкретного сервиса.</li></ul>"
}