8 lines
20 KiB
JSON
8 lines
20 KiB
JSON
{
|
||
"index": 70,
|
||
"slug": "editorial-2026-01-field-platform-api",
|
||
"title": "Как проверить совместимость платформенного API до изменения контракта",
|
||
"excerpt": "Новое поле и номер версии не доказывают совместимость. Разбираем, как сопоставить контракт с конкретным потребителем, остановить несопоставимый путь и передать проверяемый результат.",
|
||
"contentHtml": "<p>После небольшого изменения API клиент начинает показывать пустой экран. Платформенная команда добавила поле в JSON, оставила старые поля и подняла версию с <code>1.2.0</code> до <code>1.3.0</code>. Один ручной запрос вернул правильный ответ. Через час другой клиент получает новый статус, не находит запись и повторяет запрос.</p>\n<p>Цена ошибки выше, чем неудачный запрос. Клиент может сохранить неверное состояние, повторить команду или показать пользователю, что объект исчез. Команда платформы тратит время на спор о слове «совместимо», хотя не записала, что именно клиент обязан прочитать и какие ошибки должен различать.</p>\n<p>Тезис статьи короткий: совместимость принадлежит паре «контракт — конкретный потребитель». Номер версии помогает назвать поверхность изменения, но не заменяет проверку. Схема OpenAPI описывает форму HTTP API, а не закрытый парсер клиента, порядок обработки полей или смысл ошибки. Поэтому перед изменением нужно зафиксировать семью контракта, минимальные поля, разрешённые ошибки и отдельные исключения.</p>\n<h2>Сначала отделите форму ответа от его смысла</h2>\n<p>Возьмём учебный API чтения каталожной записи. Он принимает <code>recordId</code> и возвращает обязательные поля <code>id</code> и <code>state</code>. Поле <code>label</code> необязательно. Ошибка <code>fixed-not-found</code> означает только отсутствие записи. Она не означает ошибку сети, отказ в доступе или невалидный запрос.</p>\n<pre><code>GET /records/42 HTTP/1.1\nAccept: application/json\n\nHTTP/1.1 200 OK\nContent-Type: application/json\n\n{\n \"id\": \"42\",\n \"state\": \"active\",\n \"label\": \"Учебная запись\"\n}</code></pre>\n<p>Этот фрагмент показывает одну representation — передаваемую форму ресурса. Он не обещает порядок ключей, время ответа, сохранность записи или наличие поля в следующей версии. Даже наличие <code>label</code> в примере не делает его обязательным. Эти свойства нужно вынести в контракт явно. Иначе наблюдение быстро превращается в неофициальную гарантию.</p>\n<p>Потребитель должен быть описан так же точно. Например, <code>fixed-tolerant-reader-v1</code> читает только <code>id</code> и <code>state</code>, принимает версию <code>1.3.0</code> и знает ошибку <code>fixed-not-found</code>. Другой клиент требует <code>legacyMode</code>. Для него тот же ответ неполон. Третий адаптер отправляет команды, а не читает записи. Его нельзя сравнивать с read API только из-за одинакового JSON.</p>\n<h2>Минимальная карточка потребителя</h2>\n<p>Не начинайте с перечня всех команд и репозиториев. Запишите минимальное решение, которое клиент принимает по ответу. В карточке нужны четыре поля: <code>contractFamily</code>, поддерживаемая версия, обязательные поля и известные ошибки. Если клиент зависит от порядка, повторов или специального заголовка, это тоже явное требование. Если требование неизвестно, статус должен остаться неизвестным.</p>\n<figure><img src=\"/assets/editorial/2026/platform-api-2026-consumer-compatibility-loop.svg\" alt=\"Цикл проверки платформенного API: contract family, поля, гарантии и исключения ведут к ограниченному hand-off или к явной остановке\" loading=\"lazy\" /><figcaption>Проверка сначала устанавливает сопоставимость, затем сравнивает поверхность и гарантии. Она не выпускает версию и не меняет API.</figcaption></figure>\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>Сверить required fields и обработку unknown fields</td><td>Сохранить старый ответ или выпустить отдельный контракт</td></tr><tr><td>404 стал «нет записи» для всех ошибок</td><td>Клиент смешал прикладную ошибку с сетевой</td><td>Воспроизвести 404, 401, 403, 422 и timeout раздельно</td><td>Оставить fallback только для документированной ошибки</td></tr><tr><td>Похожий endpoint объявили несовместимым</td><td>Сравнили разные contract family</td><td>Проверить operation и family до сравнения полей</td><td>Вернуть <code>stop-incomparable-consumer</code></td></tr><tr><td>После minor-версии изменился смысл статуса</td><td>Новый символ или переход не вошёл в гарантию</td><td>Сверить список значений и переходы состояний</td><td>Оформить изменение как новый контракт или сохранить семантику</td></tr><tr><td>Ручной запрос успешен, релиз сломан</td><td>Проверили один пример вместо named consumer</td><td>Запустить проверку на карточке конкретного клиента</td><td>Не передавать общий verdict без причин и next action</td></tr></tbody></table></div>\n<h2>Проверяйте family до полей</h2>\n<p><code>Contract family</code> — это вид операции и её смысловая граница. Read API, командный адаптер и webhook могут иметь поля <code>id</code> и <code>state</code>, но описывают разные действия. Сначала сравните <code>fixed-catalog-read-v1</code> с тем, что объявил consumer. Если consumer относится к <code>fixed-catalog-command-v1</code>, проверка не должна доходить до полей.</p>\n<p>Такой результат не равен incompatibility. Команды пока не доказали, что объекты сопоставимы. Если назвать его просто «несовместимо», следующая команда начнёт ненужную миграцию. Точный статус сохраняет границу: нужен отдельный review для command family.</p>\n<h2>Синтетический пример отрицательного пути</h2>\n<p>Следующий код ограничен учебными объектами в памяти. Он не вызывает API, не читает production-трассы и не доказывает поведение реального клиента. Его задача — показать порядок решения: family проверяется раньше полей.</p>\n<pre><code>const api = {\n family: 'fixed-catalog-read-v1',\n version: '1.3.0',\n response: ['id', 'state'],\n errors: ['fixed-not-found']\n};\n\nconst consumer = {\n id: 'fixed-command-adapter-v1',\n family: 'fixed-catalog-command-v1',\n required: ['id', 'state']\n};\n\nfunction review(api, consumer) {\n if (api.family !== consumer.family) {\n return {\n status: 'stop-incomparable-consumer',\n nextAction: 'separate-contract-review-by-family'\n };\n }\n\n const missing = consumer.required.filter(\n (field) => !api.response.includes(field)\n );\n return missing.length\n ? { status: 'incompatible-consumer', missing }\n : { status: 'compatible-for-this-check' };\n}\n\nconsole.log(review(api, consumer));\n// { status: 'stop-incomparable-consumer',\n// nextAction: 'separate-contract-review-by-family' }</code></pre>\n<p>Положительный статус здесь тоже узкий. Он означает только, что зафиксированная пара прошла перечисленные проверки. Он не означает, что rollout безопасен, SLA выполнен или в системе нет неизвестных клиентов. В production-коде такой verdict должен сопровождаться именем consumer, версией и перечнем проверенных гарантий.</p>\n<h2>Не путайте optional с совместимостью</h2>\n<p>Слово <code>optional</code> обычно относится к конкретному валидатору или схеме. Оно не отвечает на вопрос, что сделает consumer, если поле отсутствует или появилось неожиданное поле. Один reader игнорирует расширение. Другой использует строгую модель. Третий считает отсутствие поля признаком старого режима.</p>\n<p>Предположим, что в ответ добавили <code>legacyMode</code>. Если tolerant reader его не использует, добавление может быть безопасным для этой пары. Но клиент, который требует поле, уже нельзя пометить compatible. Нельзя выводить обратное поведение из названия поля или из того, что ручной запрос всё ещё проходит.</p>\n<p>То же относится к значениям перечисления. Старый клиент может принимать <code>active</code> и <code>archived</code>, но падать на новом <code>paused</code>. В схеме поле осталось строкой, а смысл ответа изменился. Значит, проверка должна сравнивать не только наличие поля, но и допустимые значения и переходы, которые видит клиент.</p>\n<h2>Отдельно фиксируйте ошибки и исключения</h2>\n<p>Список ошибок — часть поведения consumer. Если fallback разрешён только для <code>fixed-not-found</code>, клиент не должен подставлять пустое состояние после <code>401</code>, <code>403</code>, <code>422</code> или таймаута. Иначе временный сбой превращается в потерю данных на экране, а повтор может отправить команду дважды.</p>\n<p>Иногда нужен специальный путь. Например, один внутренний инструмент получает расширенное представление. Это допустимо только как отдельный, названный и документированный контракт. Он должен описать получателя, версию, входные условия и то, чего не гарантирует. Секретный query-параметр вроде <code>?debug=1</code> не является исключением. Его обнаружит следующий consumer, но не обнаружит общий review.</p>\n<p>Если команда добавляет гарантию о стабильном порядке, времени ответа или доступности, её нельзя прятать рядом с полем. Это новая публичная обязанность. Её нужно назвать, проверить на соответствующем уровне и привязать к consumer. Один успешный trace не доказывает ни одну из этих гарантий.</p>\n<h2>Что даёт номер версии</h2>\n<p>SemVer полезен после того, как команда определила public API. Он помогает назвать совместимые добавления и несовместимые изменения. Но строка <code>1.3.0</code> сама не отвечает, является ли новый статус допустимым, игнорирует ли клиент неизвестные поля и относится ли consumer к той же семье.</p>\n<p>OpenAPI снижает догадки о форме HTTP-интерфейса: путях, параметрах, запросах, ответах и схемах. Это необходимый слой описания. Но документ не знает скрытую ветку клиентского кода. RFC 9110 также не превращает representation в гарантию прикладной совместимости. Поэтому стандарты дают словарь и границы, а итог принимает проверка конкретной пары.</p>\n<h2>Порядок действий перед изменением</h2>\n<ol><li><strong>Назовите операцию.</strong> Запишите один endpoint, действие, contract family и версию. Не смешивайте чтение, команду и webhook.</li><li><strong>Назовите consumer.</strong> Укажите систему или модуль, владельца проверки и supported versions. Не используйте «все клиенты» как идентификатор.</li><li><strong>Опишите минимальное чтение.</strong> Перечислите обязательные поля, допустимые значения, ошибки и требования к неизвестным полям.</li><li><strong>Отсечьте другую family.</strong> При различии семей верните <code>stop-incomparable-consumer</code> и откройте отдельный review.</li><li><strong>Сопоставьте поверхность.</strong> Найдите отсутствующие поля, новые значения и изменившиеся ошибки. Каждый пробел оставьте причиной, а не спрячьте под номером версии.</li><li><strong>Проверьте исключения.</strong> Для отдельного пути потребуйте имя, версию, получателя и отрицательную границу гарантий.</li><li><strong>Сформируйте hand-off.</strong> Передайте status, reasons и next action. Положительный статус не запускает rollout и не заменяет тесты потребителя.</li></ol>\n<h2>Ограничения и критерий готовности</h2>\n<p>Такой review не обнаруживает неизвестные интеграции сам по себе. Он не заменяет контрактные тесты, нагрузочные проверки, security review, миграцию данных, SLA или план отката. Учебный код выше работает на фиксированных литералах. Он не сообщает production-результат и не подтверждает, что реальный клиент действительно описал все свои зависимости.</p>\n<p>Проверку можно считать готовой только для явно ограниченной пары. В отчёте есть одна contract family, одна версия, один named consumer, список required fields, допустимые ошибки, заявленные гарантии и итоговый status. Для <code>compatible-for-this-check</code> нет неописанного claim и не осталось неизвестного обязательства. Для любого <code>stop</code> указаны причина и следующий шаг. Если хотя бы одного элемента нет, слово «совместимо» преждевременно.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://spec.openapis.org/oas/v3.1.1.html\" target=\"_blank\" rel=\"noopener noreferrer\">OpenAPI Specification v3.1.1</a> — официальное описание языка интерфейсов для HTTP API и схем request/response. Граница: спецификация описывает интерфейс, но не поведение конкретного consumer и не его migration policy.</li><li><a href=\"https://www.rfc-editor.org/rfc/rfc9110.html\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 9110: HTTP Semantics</a> — нормативное описание семантики HTTP, сообщений и representations. Граница: RFC не задаёт application-level compatibility, порядок полей или правила rollout.</li><li><a href=\"https://github.com/semver/semver/blob/7c834b3f3a4940d77ab593bc32583004d6a426a9/semver.md\" target=\"_blank\" rel=\"noopener noreferrer\">Semantic Versioning 2.0.0, pinned source commit</a> — официальная спецификация SemVer на неизменяемом commit. Граница: SemVer помогает классифицировать изменение объявленного public API, но не создаёт inventory потребителей и не доказывает совместимость.</li></ul>"
|
||
}
|