Клиенты запрашивают один ресурс в разных форматах. Как API должен выбрать представление ответа без дублирования endpoint?
API должен использовать согласование представления: клиент указывает допустимый формат ответа через заголовок Accept, а сервер выбирает подходящий вариант и сообщает фактически выбранный формат через Content-Type. Если подходящего представления нет, API возвращает ошибку согласования, обычно 406 Not Acceptable, либо применяет явно задокументированный формат по умолчанию.
Один и тот же ресурс может быть нужен разным клиентам в разных представлениях: например, веб-приложению — в JSON, а специализированному потребителю — в XML. Разделение ресурса и его представления позволяет не создавать отдельный endpoint для каждого формата и сохраняет единый адрес предметного объекта.
Такой подход также делает контракт расширяемым: новый формат можно добавить, не меняя смысл ресурса и не заставляя клиентов, использующих старый формат, немедленно обновляться.
Если формат зашит в URL или выбирается неявно, API быстро получает дублирующиеся маршруты, неоднозначные правила и сложное кэширование. Клиент может не суметь разобрать ответ, хотя сервер формально вернул успешный результат.
Отдельный риск возникает у промежуточных кэшей. Если ответ зависит от Accept, но кэш не учитывает этот заголовок, клиент может получить представление, подготовленное для другого потребителя.
Клиент передаёт предпочтения в Accept. Сервер сопоставляет их с поддерживаемыми представлениями, учитывает приоритеты форматов и возвращает выбранное представление в Content-Type.
Content-Type описывает формат тела текущего сообщения, а Accept — форматы, которые клиент готов принять. Это разные роли: Content-Type запроса, например, описывает формат передаваемого клиентом тела и не заменяет Accept.
Заголовок Vary: Accept сообщает кэшу, что результат зависит от значения Accept. Без него кэш может считать ответы для разных форматов одним вариантом. Если клиент не указал Accept, поведение должно быть определено контрактом: сервер может выбрать формат по умолчанию или считать допустимыми все поддерживаемые форматы.
Согласование формата не решает автоматически задачу версионирования семантики. JSON и XML могут быть разными представлениями одной модели, но изменение обязательных полей или смысла операции требует отдельного правила совместимости, например версии контракта.
У подхода есть компромиссы. Несколько форматов увеличивают объём тестирования, документации и поддержки; кроме того, сложные правила приоритетов могут быть непонятны клиентам. Поэтому обычно поддерживают ограниченный набор форматов и явно фиксируют поведение при отсутствии подходящего варианта.
Сервис отчётов обслуживает веб-клиент и внешнего партнёра. Веб-клиенту нужен JSON, а партнёр исторически принимает XML. Команда рассматривала два варианта: создать отдельный endpoint для XML или оставить один endpoint и применить согласование представлений.
Отдельный endpoint был проще для первоначальной реализации, но привёл бы к дублированию авторизации, документации, тестов и правил фильтрации. Единый endpoint с Accept потребовал общего слоя преобразования и настройки кэширования, зато сохранил единый контракт ресурса.
Выбрали единый endpoint, ограниченный двумя документированными форматами, с явным Content-Type, Vary: Accept и ответом 406, если формат не поддерживается. Это позволило добавить XML без копирования бизнес-логики и сохранить предсказуемое поведение клиентов.
Accept отличается от Content-Type?Accept относится к ожидаемому формату ответа. Content-Type описывает формат тела конкретного сообщения: тела ответа или, если речь о запросе, тела запроса. Например, клиент может отправить JSON с Content-Type: application/json, но попросить ответ в XML через Accept: application/xml.
Accept?Автоматический вывод о единственно правильном поведении делать нельзя. Контракт должен определить правило: сервер выбирает формат по умолчанию, считает допустимым стандартный формат или отклоняет запрос. Важно, чтобы это поведение было стабильным и отражалось в документации, иначе разные клиенты будут по-разному интерпретировать один и тот же вызов.
Content-Type недостаточно для корректного кэширования?Content-Type сообщает формат уже сформированного ответа, но не обязательно показывает, по какому параметру сервер выбрал этот вариант. Если выбор зависит от Accept, кэшу нужно указать это через Vary: Accept. Иначе он может вернуть JSON клиенту, запросившему XML, или наоборот, несмотря на корректные заголовки самого сохранённого ответа.