Перейти к содержанию

Согласование формата ответа

В предыдущем уроке один курс получил два представления: JSON для программы и HTML для человека. Теперь нужно выбрать, какое из них вернуть в конкретном ответе. Клиент сообщает приемлемые типы через Accept, а сервер сопоставляет их с доступными вариантами. Такое согласование является частью API, поскольку неправильный формат способен сломать клиента даже при успешном статусе.

Результат урока — ясное правило выбора и отказа. Рассмотрим только единичный /api/courses/c001: коллекция на этом этапе возвращает JSON. Наши сообщения остаются смысловыми HTTP/1.1 иллюстрациями. В HTTP/2 и HTTP/3 тот же договор о методе и полях будет иметь другое физическое кодирование.

Явный запрос JSON

Программный клиент, умеющий только JSON, может объявить это без вариантов. Вот полный смысловой запрос чтения:

GET /api/courses/c001 HTTP/1.1
Host: catalog.example
Accept: application/json

Сервис выбирает JSON, указывает его тип, валидатор и зависимость выбора от Accept. Тело соответствует снимку s1:

HTTP/1.1 200 OK
Content-Type: application/json
Vary: Accept
ETag: "c001-json-r1"
Cache-Control: public, no-cache

{"id":"c001","title":"Современный JavaScript","topic":"frontend","lessons":40}

Клиент не должен делать вывод о формате только из расширения пути. В адресе нет .json, но тип указан явно. Если реализация решит включить расширение как отдельный маршрут, это станет новым правилом выбора; в нашем договоре достаточно поля запроса. Важно не держать два противоречащих друг другу механизма без описания приоритета.

При отсутствии Accept наш сервис использует JSON по умолчанию. Это собственное правило сервиса при разрешении любого подходящего формата, а не утверждение, что все серверы обязаны выбирать JSON. Если клиенту принципиален конкретный тип, он должен отправить его явно. Значение по умолчанию удобно человеку, но не заменяет договор программного клиента.

Предпочтение среди доступных вариантов

Клиент способен назвать несколько приемлемых типов и веса q. В данном примере HTML предпочтительнее JSON:

GET /api/courses/c001 HTTP/1.1
Host: catalog.example
Accept: text/html;q=1, application/json;q=0.5

Ожидается HTML-документ из предыдущего урока. Ответ содержит Content-Type: text/html; charset=utf-8, Vary: Accept и собственную метку "c001-html-r1". JSON клиент тоже принимает, но сервис имеет более предпочтительный вариант. Число q задаёт относительное предпочтение, а не процент качества документа или скорость соединения.

Для совпадающих весов наш сервис выбирает JSON как внутренний порядок предпочтения. Этот дополнительный выбор требуется объявить, чтобы одинаковые запросы не давали случайный формат. Сначала определяются применимые диапазоны типов и их веса, затем доступные приемлемые кандидаты, затем правило разрешения равенства. Простое чтение первой подстроки заголовка не воспроизводит эти шаги.

Диапазон */* принимает любой тип, а text/* — подходящие текстовые варианты. Более конкретное правило имеет значение при оценке кандидата. Например, application/json;q=0, */*;q=1 не должно разрешить JSON через общий диапазон: точное правило исключает его. В нашем сервисе остаётся HTML. Клиент, который поставил q=0, не просит «самый слабый вариант», а объявляет неприемлемость этого типа.

Фактическую обработку заголовка следует поручить корректному механизму библиотеки или тщательно определённому алгоритму. Урок задаёт договор выбора, а не предлагает небезопасный парсер на нескольких split. Ошибки вокруг диапазонов, параметров и повторов легко приводят к тому, что сервер возвращает запрещённый клиентом формат. Правила Accept.

Когда подходящего варианта нет

Изменим только поле Accept: клиент теперь принимает XML. Наш единичный ресурс поддерживает JSON и HTML, но XML не формирует. Для учебного договора выбираем 406 Not Acceptable.

GET /api/courses/c001 HTTP/1.1
Host: catalog.example
Accept: application/xml
HTTP/1.1 406 Not Acceptable
Vary: Accept
Content-Length: 0

Мы специально выбрали пустое тело данного отказа: клиент запретил все предлагаемые выходные типы. Не заставляем его читать JSON-ошибку после заявления «принимаю только XML». Это не обязательная форма любого 406; в других договорах можно описать доступные представления подходящим образом. Здесь ноль действительно соответствует отсутствию содержимого, поэтому не требуется вычислять размер текста.

Пустое тело не отменяет конкретного результата. Клиент знает статус и может объяснить несовместимость формата. Существование курса при этом не изменилось: повтор с Accept application/json возвращает его обычное представление. Поэтому 406 нельзя превратить в 404 или записать как удаление записи из интерфейса.

Для ошибок других операций наше API использует application/problem+json. Программный клиент должен предусмотреть этот формат отдельно от успешного JSON. При строгой политике согласования можно посылать Accept: application/json, application/problem+json; тип с суффиксом +json не тождественен буквальному application/json. Такое различие помогает не сделать случайное исключение в обработке отказов.

Отсутствие заголовка и ошибка парсера

Отсутствующий Accept и заголовок с некорректным синтаксисом не являются одной ситуацией. В первом случае нет ограничения формата со стороны этого поля, и действует выбранный JSON по умолчанию. Во втором реализация должна иметь последовательную политику обработки повреждённого входа; наш сервис рассматривает неразбираемый Accept как 400, не как произвольный выбор первого типа.

Программному клиенту полезно отличать неприемлемый формат от неуспешного предметного действия. Если он попросил XML и получил 406, курс не был изменён и не пропал. Если POST с поддерживаемым JSON отверг lessons, формат входа принят, но значения нет. Эти различия определяют, какую настройку исправлять: ожидания формата или документ операции.

Можно намеренно проверить влияние весов, не выполняя сеть: составьте два одинаковых списка с разными q для HTML и JSON и примените объявленный алгоритм выбора на бумаге. При q=0 у обоих доступных типов получится отказ, а при отсутствии q вес по правилам поля считается единицей. Серверное предпочтение используется только после определения приемлемых вариантов, а не для обхода запрета.

В реальном браузерном переходе Accept способен отличаться от значения программного fetch, даже когда адрес одинаков. Поэтому наблюдение «в адресной строке пришёл HTML» не опровергает JSON-контракт клиента, явно задающего application/json. Сравнивать нужно весь существенный контекст запроса, а не только путь.

В нашем договоре язык всегда русский, и выбор не зависит от User-Agent. Это сохраняет простую связь Vary с Accept. Если разработчик позже введёт скрытое ветвление по другим полям, потребуется обновить метаданные и проверку вариантов вместе с генератором. Один новый условный оператор в шаблоне способен изменить правильность кеширования всего ресурса.

Входные данные согласуются отдельно

Теперь рассмотрим POST, у которого клиент отправляет XML, но принимает JSON. Accept не исправляет неверный входной тип:

POST /api/courses HTTP/1.1
Host: catalog.example
Content-Type: application/xml
Accept: application/json, application/problem+json

<course><title>HTTP и API</title></course>

Сервис принимает для создания только JSON, поэтому выбирает 415 Unsupported Media Type. Причина находится в содержимом запроса, а не в представлении ответа. Если заменить только Accept на HTML, XML по-прежнему не станет приемлемым входом. Если заменить тело и Content-Type на JSON, вопрос выхода будет решаться отдельно.

Именно это разделение позволяет создавать предсказуемые клиенты. Один обработчик сериализует вход, другой сообщает приемлемые ответы, третий проверяет полученный Content-Type. Универсальный флаг «JSON-запрос» часто скрывает все три разных решения и затрудняет объяснение отказа. Назвать каждое действие отдельно полезнее, чем запомнить одну настройку библиотеки.

Vary: Accept обязательно сохраняет связь между чтением и выбором. Если кеш сохранил HTML по пути c001, следующий клиент с Accept JSON не должен получить его как подходящую копию. ETag тоже принадлежит выбранному варианту. HTML-метка не подтверждает неизменность JSON; в условном чтении будет использоваться метка фактически сохранённого представления.

Мы пока не добавляем языки и сжатие как новые измерения выбора. Если они понадобятся, потребуется рассмотреть соответствующие поля, Vary и валидаторы вместе. Добавление локализации только в генератор тела оставит кеш с неполным ключом. Теперь формат ответа определён; следующим уроком закрепим смысл полей JSON, чтобы два клиента одинаково понимали уже выбранный вариант.

Оглавление курса