Заголовки и метаданные сообщения
Статус определяет общий исход обработки, но для чтения ответа этого недостаточно. Клиенту нужно узнать формат содержимого, адрес созданной записи и правила повторного использования. Эти сведения HTTP передаёт полями заголовков. В этом уроке разберём их роль на знакомом каталоге и отделим метаданные сообщения от предметных свойств курса.
Результатом будет согласованная схема запроса и ответа, в которой каждое поле отвечает на конкретный вопрос. Мы не создаём собственный HTTP-парсер и не переписываем защитную политику сайта. Учебные листинги показывают смысловые поля; физические границы передачи, размер и кодирование транспортной версии остаются работой будущей реализации.
Тип входа и приемлемый выход
Начнём с создания курса. В запросе две строки, которые часто путают: Content-Type и Accept. Первая описывает содержимое этого сообщения, вторая — приемлемые представления будущего ответа.
POST /api/courses HTTP/1.1
Host: catalog.example
Content-Type: application/json
Accept: application/json
{"title":"HTTP и API","topic":"backend","lessons":32}
Клиент посылает JSON и хочет получить JSON. Совпадение значений является удобным частным случаем, но не обязанностью HTTP. Можно отправить JSON, а получить HTML, если сервис поддерживает такой результат и клиент его принимает. Нельзя решить, что тело запроса является JSON, только по наличию Accept: это поле не описывает уже переданные данные.
После принятого создания сервер сообщает тип своего тела и местоположение новой записи. Эти сведения читают отдельно от JSON:
HTTP/1.1 201 Created
Location: /api/courses/c005
Content-Type: application/json
Cache-Control: no-store
{"id":"c005","title":"HTTP и API","topic":"backend","lessons":32}
Location здесь содержит относительную URI-ссылку, разрешаемую относительно адреса запроса. Она не обязательно является полным URL. Статус 201 сообщает создание; наличие Location не превращает его в перенаправление. Поведение перехода определяется семантикой ответа, а не одним фактом присутствия поля.
Cache-Control: no-store в данном ответе выбран как политика результата редактирования. Он не удаляет существующие файлы пользователя и не отзывает уже показанный объект. Его задача — указать участвующим HTTP-кешам, что этот ответ не следует сохранять. Политика для общедоступного чтения может быть иной: один набор заголовков не обязан подходить всем операциям.
Имя поля и его значение
Имена полей HTTP регистронезависимы: Content-Type и content-type обозначают одно поле. Это не означает, что любые значения также можно бездумно привести к нижнему регистру. Значение может содержать регистрозависимый идентификатор, подписанную строку или непрозрачную метку. Читайте правила конкретного поля, прежде чем преобразовывать его.
Порядок разных полей обычно не определяет семантику. Однако повторение одного имени и объединение значений имеют специальные правила; нельзя превращать все строки в один объект и всегда склеивать их запятыми. Хороший пример ограничения — Set-Cookie, у которого такое объединение повредит форму. В нашей первой части cookies ещё не используются, но будущий сервер должен обращаться с заголовками через подходящую библиотеку.
Из-за этого урок не предлагает функцию, делящую сырой текст по двоеточию и считающую результат надёжным HTTP. В реальном сообщении есть ограничения синтаксиса, размера и обрамления. Ошибки на этой границе могут затрагивать посредников. Проектируя API, мы определяем значения, а корректную передачу доверяем протокольному стеку. Поля HTTP и их назначение.
Метаданные чтения
Для изменяемого общедоступного курса договор допускает хранение с проверкой перед повторным использованием. Покажем полный предметный ответ c001 с добавленными смысловыми полями:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, no-cache
Vary: Accept
ETag: "c001-json-r1"
{"id":"c001","title":"Современный JavaScript","topic":"frontend","lessons":40}
no-cache не равен no-store. В этой схеме ответ можно сохранить, но перед повторным использованием требуется предусмотренная проверка. В уроке об условном GET клиент отправит сохранённую метку, а сервер решит, нужно ли пересылать новое тело. Пока видим, какие сведения понадобятся для такого решения.
ETag — непрозрачный валидатор выбранного представления. Название метки в учебном примере читаемо для человека, но клиент не должен вычислять номер следующей версии или извлекать бизнес-смысл из строки. Он хранит точное значение вместе с представлением. Если появится другой формат того же курса, его валидатор должен соответствовать именно его данным.
Vary: Accept говорит, что выбор ответа зависит от этого поля запроса. Общий кеш не должен считать JSON и HTML взаимозаменяемыми только потому, что путь совпал. Вариантность особенно важна при согласовании форматов: наличие тела в кеше ещё не означает, что оно подходит нынешнему клиенту. Подробные правила хранения рассматриваются отдельно, здесь мы фиксируем связь выбора и метаданных.
Поля не одинаковы для всех получателей
По пути сообщения могут находиться посредники. Некоторые сведения относятся к выбранному представлению и нужны конечному получателю, другие управляют конкретной связью. Например, Cache-Control описывает правила использования ответа, а историческое Connection относится к управлению соответствующим соединением HTTP/1.x. Переносить все поля одинаково между разными версиями протокола нельзя.
Поэтому заголовок Connection не подходит для объявления предметного режима курса. Если приложение хочет сказать, что курс доступен только редактору, это правило доступа и модель ответа, а не управление жизнью TCP-соединения. Смешивание уровней делает контракт непонятным и способно нарушить передачу через посредника.
В диагностике полезно видеть также различие времени ответа и возраста сохранённого сообщения. Поле Age, когда оно применимо, помогает объяснить использование HTTP-кеша; оно не является возрастом учебного материала. Старый курс может иметь только что сформированный ответ, а недавно отредактированная страница — копию, удерживаемую по принятой политике. Предметные даты требуют самостоятельных полей.
Наконец, произвольный клиент не всегда вправе напрямую задать любой протокольный заголовок. Браузер управляет частью сведений сам, включая детали запроса и соединения. Текстовые схемы помогают читать результат, но не обещают, что каждую показанную строку можно вручную назначить через fetch. Будущий клиент должен учитывать правила своей среды и использовать API библиотеки по назначению.
Служебные сведения и предметная модель
Количество уроков остаётся полем JSON lessons. Оно не превращается в Content-Length. Последнее, когда используется, описывает размер передаваемого содержимого в байтах; кириллица и сжатие делают подсчёт «по символам» ошибочным. Мы не добавляем выдуманное числовое значение в иллюстрацию. Будущая библиотека рассчитает его по фактически сериализованным данным либо применит другое допустимое обрамление.
Аналогично время изменения курса и Date ответа отвечают на разные вопросы. Date относится к формированию HTTP-сообщения; бизнес-поле могло бы означать момент редакторского изменения. Подмена одного другим ломает отчётность и сравнение версий. При необходимости оба значения должны иметь отдельно определённый смысл.
Запрос неподдерживаемого метода вызывает ответ 405 с Allow. Это метаданные возможностей целевого ресурса, а не разрешение пользователя. Если Allow включает PUT, а пользователь не редактор, проверка прав всё равно должна отказать. Заголовки помогают описывать механизм, но не отменяют прикладной контроль.
Защитные поля вроде CSP относятся к ограничениям браузерного документа и изучались в уроке о политике браузера. Не стоит переносить его набор без рассмотрения типа ответа. JSON API и страница с формами имеют разные задачи; текущий урок выделяет метаданные обмена, необходимые для корректного клиента.
Теперь измените только Accept первого запроса и спросите, изменилось ли содержимое входного JSON. Затем измените только Content-Type, сохранив байты тела: это уже вопрос о соответствии объявленного входа. Такой мысленный разбор обнаруживает ошибку раньше запуска сервера. В следующем уроке уточним, почему один ресурс способен иметь несколько представлений и какие свойства должны оставаться общими.