8 lines
18 KiB
JSON
8 lines
18 KiB
JSON
{
|
||
"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 "id": "customer-17",\n "revision": 4,\n "state": "active"\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 < 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>"4"</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>"
|
||
}
|