HTTP: ресурс, представление и результат запроса
Оглавление · Кеширование · API проекта
Задача: прочитать обмен клиент/сервер и определить намерение операции, результат и границу доверия. Нужны Promise, JSON и runtime-валидация.
Ресурс не равен строке JSON
Ресурс — то, к чему обращаются; представление — передаваемое описание его состояния. URL идентифицирует адрес обращения, метод задаёт намерение, заголовки — метаданные, тело — содержимое. Content-Type описывает формат тела, Accept — предпочтения клиента. HTTP/1.1, HTTP/2 и HTTP/3 меняют передачу сообщений, сохраняя общую семантику. HTTP Semantics.
Рассмотрим учебный адрес /api/articles/sample. Это будущий пример контракта,
такого обработчика в Atmanki нет. Одна статья может иметь HTML для читателя
и JSON для клиента; ни одно представление не обязано совпадать со строкой БД.
Публичный JSON не должен включать редакторские поля только потому, что они есть в CMS.
GET /api/articles/sample HTTP/1.1
Host: example.test
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "article-v7"
{"slug":"sample","title":"Учебная статья"}
Это текстовая иллюстрация, не побайтовый HTTP-пакет с рассчитанной длиной. Настоящий клиент и сервер формируют служебные поля и кодирование.
Метод выражает намерение
| Метод | Намерение | Безопасный | Идемпотентный по семантике |
|---|---|---|---|
| GET | Получить представление | Да | Да |
| HEAD | Как GET, без тела ответа | Да | Да |
| POST | Обработать содержимое по правилам ресурса | Нет | Не обязательно |
| PUT | Создать/заменить состояние указанного ресурса | Нет | Да |
| DELETE | Удалить связь с ресурсом | Нет | Да |
| PATCH | Применить описанные изменения | Нет | Не обязательно |
Безопасность здесь означает отсутствие запрошенного изменения состояния; логи возможны. Идемпотентность относится к предполагаемому эффекту повторов, а не одинаковым ответам. Методы. PATCH может быть спроектирован идемпотентным, но не обладает этим свойством по умолчанию. Формат patch-документа определяет смысл изменения. RFC 5789.
В учебном редакторе «установить заголовок X» и «добавить единицу к просмотрам»
дают разные переходы. Второй DELETE может сообщить, что ресурс уже отсутствует,
хотя требуемое конечное состояние сохранено. GET /delete?id=sample нарушает
ожидания метода: ссылку могут открыть предварительная загрузка или робот.
Кешируемость — отдельный вопрос: её нельзя вывести только из идемпотентности. Правила HTTP-кеша разобраны в предыдущей главе.
Статус и прикладной результат
| Статус | Смысл для клиента |
|---|---|
| 200 | Успешное выполнение; формат результата задан контрактом |
| 201 | Создан ресурс; Location может указать его адрес |
| 202 | Принято в обработку; завершение ещё не подтверждено |
| 204 | Успех без тела ответа |
| 400 / 415 / 422 | Неверный запрос / неподдерживаемый формат / необрабатываемое содержимое |
| 401 / 403 | Нужна аутентификация / доступ запрещён |
| 404 | Ресурс не найден либо его существование не раскрывается |
| 409 / 412 | Конфликт состояния / не выполнено предусловие |
| 429 | Превышен лимит запросов |
| 500 / 503 | Внутренняя ошибка / временная недоступность |
Это ориентиры, а не универсальная таблица повторов.
Справочник статусов.
Конкретный контракт уточняет тело ошибки и допустимое действие клиента.
У 204 нельзя безусловно вызывать response.json(); у 202 нужен отдельный способ
узнать итог. JSON с полем ok: false не превращает HTTP 200 в HTTP-ошибку.
Исходник схемы
flowchart TD
Request["Запрос"] --> Transport{"Получен HTTP-ответ?"}
Transport -->|Нет| Unknown["Исход операции может быть неизвестен"]
Transport -->|Да| Status["Статус и заголовки"]
Status --> Decode["Тело по контракту, если оно предусмотрено"]
Decode --> Result["Прикладной результат и ошибки"]
Ошибка сети, HTTP-ошибка, неверный формат и прикладной отказ — разные случаи. Не маскируйте всё одним «не найдено» и не показывайте клиенту внутренний stack trace. Для машиночитаемых ошибок можно выбрать Problem Details: тип проблемы, статус, заголовок и детали. Это формат, а не готовая классификация бизнес-ошибок. RFC 9457.
Условное обновление
Два редактора читают версию v7. Первый сохраняет новую v8; второй отправляет
изменение с If-Match: "article-v7". При несовпадении текущего валидатора сервер
отклоняет предусловие, вместо молчаливой потери первого изменения.
If-Match.
Сравнение версии и запись должны быть согласованы в хранилище: проверка в одном чтении и безусловная запись позже оставляют гонку. Идемпотентный PUT сам по себе не защищает от конкурирующего редактора. В Atmanki такой endpoint и механизм If-Match пока не реализованы.
Где проверяется доверие
Аутентификация отвечает «кто обращается», авторизация — «что ему разрешено».
Схема Zod отвечает на другой вопрос: подходит ли значение заявленной форме.
Проверка slug: string не разрешает чтение любой закрытой записи.
В проекте webhook проверяет Bearer secret на сервере, затем JSON и общую схему.
Контекст tRPC пока {}: будущая операция записи потребует отдельной модели доступа.
Cookies, CORS и CSRF подробно разобраны в главе о браузерных границах;
не считайте наличие Origin или типизированного клиента авторизацией.
Практика на существующем коде
Сравните health, content-health и webhook. Первый отвечает без CMS-чтения; второй возвращает 200/503 и no-store; третий различает отсутствие настройки, неверный доступ, неверный вход и ошибку инвалидирования. Успех webhook подтверждает приём и обработку уведомления, а не обновление каждого открытого окна.
На локальном сайте выполните только чтение:
curl --include http://localhost:3000/api/health
curl --include http://localhost:3000/api/content-health
Запишите статус, Content-Type, Cache-Control и смысл тела. Сопоставьте ответ
с кодом, не делая вывода о PostgreSQL из одного /api/health.
Затем спроектируйте на бумаге условное изменение учебной статьи и ответ на
конфликт редакторов. Это упражнение, не новая production-операция.