From 019134b2e8b849654409e2e686f1f5afd3ae7c33 Mon Sep 17 00:00:00 2001 From: Sergey Konstantinov Date: Thu, 1 Jun 2023 10:29:20 +0300 Subject: [PATCH] HTTP semantics started --- .../03.md | 39 +++- .../03.md | 173 +++++++++++++++++- .../05-Раздел IV. HTTP API и REST/02.md | 0 .../05-Раздел IV. HTTP API и REST/03.md | 172 ----------------- 4 files changed, 210 insertions(+), 174 deletions(-) delete mode 100644 src/ru/drafts/05-Раздел IV. HTTP API и REST/02.md diff --git a/src/en/clean-copy/05-[Work in Progress] Section IV. The HTTP API & REST/03.md b/src/en/clean-copy/05-[Work in Progress] Section IV. The HTTP API & REST/03.md index f9c9b8f..abf9b22 100644 --- a/src/en/clean-copy/05-[Work in Progress] Section IV. The HTTP API & REST/03.md +++ b/src/en/clean-copy/05-[Work in Progress] Section IV. The HTTP API & REST/03.md @@ -1 +1,38 @@ -### The Semantics of the HTTP Request Components \ No newline at end of file +### [Components of an HTTP Request and Their Semantics][http-api-requests-semantics] + +Third import exercise we must conduct is to describe the format of HTTP request and response and explain basic concepts. Many of these might look obvious to the reader. However, the situation is that even the basic knowledge we require to move further is scattered across vast and fragmented documentation, making even experienced developers struggle with some nuances. Below, we will try to compile some structured overview sufficient to design HTTP APIs. + +To describe semantics and formats, we will refer to brand new [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html) that replaced non less than nine previous specifications dealing with different aspects of the technology. Still, a significant volume of additional functionality is nevertheless covered by separate standards. In particular, the HTTP caching principles are described in standalone [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html), whereas the popular `PATCH` method is omitted in the main RFC and is regulated by [RFC 5789](https://www.rfc-editor.org/rfc/rfc5789.html). + +An HTTP request stands for (1) applying a specific verb to a URL, stating (2) the protocol version, (3) additional meta-information in headers, and (4) optionally, some content (request body): + +``` +POST /v1/orders HTTP/1.1 +Host: our-api-host.tld +Content-Type: application/json + +{ + "coffee_machine_id": 123, + "currency_code": "MNT", + "price": "10.23", + "recipe": "lungo", + "offer_id": 321, + "volume": "800ml" +} +``` + +An HTTP response to such a request will comprise (1) protocol version, (2) status code with a corresponding message, (3) response headers, and (4) optionally, response content (body): + +``` +HTTP/1.1 201 Created +Location: /v1/orders/123 +Content-Type: application/json + +{ + "id": 123 +} +``` + +**NB**: in HTTP/2 (and future HTTP/3) instead of holistic text format, separate binary frames are used for headers and data. This doesn't affect the architectural concepts we will describe below. However, to avoid ambiguity we will give all examples in the HTTP/1.1 format. You may read about the HTTP/2 format in detail [here](https://hpbn.co/http2/). + +##### URL \ No newline at end of file diff --git a/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/03.md b/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/03.md index 2cc4663..1d2a9f5 100644 --- a/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/03.md +++ b/src/ru/clean-copy/05-[В разработке] Раздел IV. HTTP API и REST/03.md @@ -1 +1,172 @@ -### Составляющие HTTP запросов и их семантика \ No newline at end of file +### [Составляющие HTTP запросов и их семантика][http-api-requests-semantics] + +Третье важное подготовительное упражнение, которое мы должны сделать — это дать описание формата HTTP-запросов и ответов и прояснить базовые понятия. Многое из написанного ниже может показаться читателю самоочевидным, но, увы, специфика протокола такова, что даже базовые сведения о нём, без которых мы не сможем двигаться дальше, разбросаны по обширной и фрагментированной документации, и даже опытные разработчики могут не знать тех или иных нюансов. Ниже мы попытаемся дать структурированный обзор протокола в том объёме, который необходим нам для проектирования HTTP API. + +В описании семантики и формата протокола мы будем руководствоваться свежевышедшим [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html), который заменил аж девять предыдущих спецификаций, описывавших разные аспекты технологии (при этом большое количество различной дополнительной функциональности всё ещё покрывается отдельными стандартами. В частности, принципы HTTP-кэширования описаны в отдельном [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html), а широко используемый в API метод `PATCH` так и не вошёл в основной RFC и регулируется [RFC 5789](https://www.rfc-editor.org/rfc/rfc5789.html)). + +HTTP-запрос представляет собой (1) применение определённого глагола к URL с (2) указанием версии протокола, (3) передачей дополнительной мета-информации в заголовках и, возможно, (4) каких-то данных в теле запроса: + +``` +POST /v1/orders HTTP/1.1 +Host: our-api-host.tld +Content-Type: application/json + +{ + "coffee_machine_id": 123, + "currency_code": "MNT", + "price": "10.23", + "recipe": "lungo", + "offer_id": 321, + "volume": "800ml" +} +``` + +Ответом на HTTP-запрос будет являться конструкция, состоящая из (1) версии протокола, (2) статус-кода ответа, (3) сообщения, (4) заголовков и, возможно, (5) тела ответа: + +``` +HTTP/1.1 201 Created +Location: /v1/orders/123 +Content-Type: application/json + +{ + "id": 123 +} +``` + +**NB**: в HTTP/2 (и будущем HTTP/3) вместо единого текстового формата используются отдельные бинарные фреймы для передачи заголовков и данных. Этот факт не влияет на излагаемые архитектурные принципы, но во избежание двусмысленности мы будем давать примеры в формате HTTP/1.1. Подробнее о формате HTTP/2 можно прочитать [здесь](https://hpbn.co/http2/). + +##### URL + +URL — единица адресации в HTTP API (некоторые евангелисты технологии даже используют термин «пространство URL» как синоним для Мировой паутины). Предполагается, что API в парадигме REST должно использовать систему адресов столь же гранулярную, как и предметная область; иными словами, у любых сущностей, которыми мы можем манипулировать независимо, должен быть свой URL. + +Формат URL регулируется [отдельным стандартом](https://url.spec.whatwg.org/), который развивает независимое сообщество Web Hypertext Application Technology Working Group (WHATWG). Считается, что концепция URL (вместе с понятием универсального имени ресурса, URN) составляет более общую сущность URI (универсальный идентификатор ресурса). (Разница между URL и URN заключается в том, что URL позволяет *найти* некоторый ресурс в рамках некоторого протокола доступа, в то время как URN — «внутреннее» имя объекта, которое само по себе никак не помогает получить к нему доступ.) + +URL принято раскладывать на составляющие, каждая из которых может отсутствовать: + * схема (scheme) — протокол обращения (в нашем случае всегда `https:`); + * хост (host; домен или IP-адрес), к которому обращён запрос — самая крупная единица адресации; + * домен может включать в себя поддомены; + * порт (port); + * путь (path) — часть URL между доменным именем (с портом) и символами `?`, `#` или концом строки + * путь принято разбивать по символу `/` и работать с каждой частью как отдельным токеном — но стандарт этого, вообще говоря, не предписывает; + * пути с символом `/` и без символа `/` в конце (скажем `/root/leaf` и `/root/leaf/`) с точки зрения стандарта являются разными (и URL, отличающиеся только наличием/отсутствием слэша, считаются разными URL), хотя практически нам неизвестны аргументы в пользу того, чтобы не считать такие пути эквивалентными; + * пути могут содержать секции `.` и/или `..`, которые предлагается трактовать по аналогии с такими же символами в путях на файловой системе (и, соответственно, считать URL `/root/leaf`, `/root/./leaf`, `/root/branch/../leaf` эквивалентными), но стандарт этого вновь не предписывает; + * запрос (query) — часть URL после знака `?` до знака `#` или конца строки; + * query принято раскладывать на пары `ключ=значение`, разделённые символом `&`; следует вновь иметь в виду, что стандарт не предписывает query строго соответствовать этому формату; + * фрагмент (fragment; также якорь, anchor) — часть URL после знака `#`; + * фрагмент традиционно рассматривается как адресация внутри запрошенного документа, поэтому многими агентами опускается при выполнении запроса; + * два URL с различными значениями фрагмента могут считаться одинаковыми — а могут не считаться, зависит от контекста. + +В HTTP-запросах, как правило (но не обязательно) схема, хост и порт опускаются (и считаются совпадающими с параметрами соединения). (Это соглашение, кстати, Филдинг считает самой большой проблемой дизайна протокола.) + +**NB**: в стандарте также перечислены разнообразные исторические наслоения (например, передача логинов и паролей в URL или использование не-UTF кодировки), которые нам в рамках вопросов дизайна API неинтересны. Также стандарт содержит правила сериализации, нормализации и сравнения URL, которые в целом полезно знать разработчику HTTP API. + +##### Заголовки + +Заголовки — это *метаинформация*, привязанная к запросу или ответу. Она может описывать какие-то свойства передаваемых данных (например, `Content-Length`), дополнительные сведения о клиенте или сервере (`User-Agent`, `Date`), или просто содержать поля, не относящиеся непосредственно к смыслу запроса/ответа (например, `Authorization`). + +Важное свойство заголовков — это возможность считывать их до того, как получено тело сообщения. Таким образом, заголовки могут, во-первых, сами по себе влиять на обработку запроса или ответа, и ими можно относительно легко манипулировать при проксировании — и многие сетевые агенты действительно это делают, добавляя или модифицируя заголовки по своему усмотрению (в частности, современные веб-браузеры добавляют к запросам целую коллекцию заголовков: `User-Agent`, `Origin`, `Accept-Language`, `Connection`, `Referer`, `Sec-Fetch-*` и так далее, а современное ПО веб-серверов, в свою очередь, автоматически добавляет или модифицирует такие заголовки как `X-Powered-By`, `Date`, `Content-Length`, `Content-Encoding`, `X-Forwarded-For`). + +Подобное вольное обращение с заголовками создаёт определённые проблемы, если ваш API предусматривает передачу дополнительных полей метаданных, поскольку придуманные вами имена полей могут случайно совпасть с какими-то из существующих стандартных имён (или ещё хуже — в будущем появится новое стандартное поле, совпадающее с вашим). Долгое время во избежание подобных коллизий использовался префикс `X-`; уже более 10 лет как эта практика объявлена устаревшей и не рекомендуется к использованию (см. подробный разбор вопроса в [RFC 6648](https://www.rfc-editor.org/rfc/rfc6648)), однако отказа от этого префикса по факту не произошло (и многие широко распространённые нестандартные заголовки, например, `X-Forwarded-For`, его всё ещё содержат). Таким образом, использование префикса `X-` вероятность коллизий снижает, но не устраняет. Тот же RFC вполне разумно предлагает использовать вместо `X-` префикс в виде имени компании. (Мы со своей стороны склонны рекомендовать использовать оба префикса в формате `X-ApiName-Field`; префикс `X-` для читабельности [чтобы отличать специальные заголовки от стандартных], а префикс с именем компании или API — чтобы не произошло коллизий с каким-нибудь другим нестандартным префиксом.) + +Помимо прочего заголовки используются как управляющие конструкции — это т.н. «content negotiation», т.е. договорённость клиента и сервера о формате ответа (через заголовки `Accept*`) и условные запросы, позволяющие сэкономить трафик на возврате ответа целиком или частично (через заголовки `If-*`-заголовки, такие как `If-Range`, `If-Modified-Since` и так далее). + +##### HTTP-глаголы + +Важнейшая составляющая HTTP запроса — это глагол (метод), описывающий операцию, применяемую к ресурсу. RFC 9110 описывает восемь глаголов (`GET`, `POST`, `PUT`, `DELETE`, `HEAD`, `CONNECT`, `OPTIONS` и `TRACE`), из которых нас как разработчиков API интересует первые четыре. `CONNECT`, `OPTIONS` и `TRACE` — технические методы, которые очень редко используются в HTTP API (за исключением `OPTIONS`, который необходимо реализовать, если необходим доступ к API из браузера). Теоретически, `HEAD` (метод получения *только метаданных*, то есть заголовков, ресурса) мог бы быть весьма полезен в HTTP API, но по неизвестным нам причинам практически в этом смысле не используется. + +Помимо RFC 9110, множество других RFC предлагают использовать дополнительные HTTP-глаголы (такие, например, как `COPY`, `LOCK`, `SEARCH` — полный список можно найти [здесь](http://www.iana.org/assignments/http-methods/http-methods.xhtml)), однако из всего разнообразия предложенных стандартов лишь один имеет широкое хождение — метод `PATCH`. Причины такого положения дел довольно тривиальны — этих пяти методов (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`) достаточно для почти любого HTTP API. + +HTTP-глагол определяет два важных свойства HTTP-вызова: + * его семантику (что представляет собой операция); + * его побочные действия, а именно: + * является ли запрос модифицирующим (и можно ли кэшировать ответ); + * является ли запрос идемпотентным. + +| Глагол | Семантика | Безопасный (немодифицирующий) | Идемпотентный | Имеет тело | +|--------|----------------------------------|---------------|---------------|------------| +| GET | Возвращает представление ресурса | да | да | нет | +| PUT | Заменяет (полностью перезаписывает) ресурс согласно данным, переданным в теле запроса | да | да | да | +| DELETE | Удаляет ресурс | да | да | нет | +| POST | Обрабатывает запрос в соответствии со своим внутренним устройством | да | нет | да | +| PATCH | Модифицирует (частично перезаписывает) ресурс согласно данным, переданным в теле запроса | да | нет | да | + +Важное свойство модифицирующих идемпотентных глаголов — это то, что **URL запроса является его ключом идемпотентности**. `PUT /url` полностью перезаписывает ресурс так, что `GET /url` возвращает представление, соответствующее переданном в `PUT /url` телу (в случае JSON-over-HTTP API это, как правило, просто означает, что `GET /url` возвращает в точности тот же контент, чтобы передан в `PUT /url`, с точностью до значений полей по умолчанию). `DELETE /url` обязан удалить указанный ресурс — так, что `GET /url` должен вернуть `404` или `410`. + +Идемпотентность и симметричность методов `GET` / `PUT` / `DELETE` влечёт за собой запрет `GET` и `DELETE` иметь тело запроса (поскольку этому телу невозможно приписать никакой осмысленной роли). Однако (по-видимому в связи с тем, что многие разработчики попросту не знают семантику этих методов) распространённое ПО веб-серверов обычно разрешает этим методам иметь тело запроса и транслирует его дальше к коду обработки эндпойнта (использование этой практики мы решительно не рекомендуем). + +Достаточно очевидным образом ответы на модифицирующие запросы не кэшируются (хотя при определённых условиях закэшированный ответ метода `POST` может быть использован при последующем `GET`-запросе) и, таким образом, повторный `POST` / `PUT` / `DELETE` / `PATCH` запрос обязательно будет доставлен до конечного сервера (ни один промежуточный агент не имеет права ответить из кэша). В случае `GET`-запроса это, вообще говоря, неверно — гарантией может служить только наличие в ответе директив кэширования `no-store` или `no-cache`. + +Один из самых частых антипаттернов разработки HTTP API — это использование HTTP-глаголов в нарушение их семантики: + * размещение модифицирующих операций за `GET` + * промежуточные агенты могут ответить на такой запрос из кэша, если какая-то из директив кэширования отсутствует, либо, напротив, повторить запрос при получении сетевого таймаута; + * некоторые агенты считают себя вправе переходить по таким ссылкам без явного волеизъявления пользователя или разработчика; например, социальные сети и мессенджеры выполняют такие вызовы для генерации оформления ссылки, если пользователь пытается ей поделиться; + * размещение неидемпотентных операций за идемпотентными методами `PUT` / `DELETE` + * хотя промежуточные агенты редко автоматически повторяют модифицирующие запросы, тем не менее это легко может сделать используемый разработчиком клиента или сервера фреймворк; + * обычно эта ошибка сочетается с наличием у запроса тела (чтобы всё-таки отличать, что конкретно нужно перезаписать или удалить), что является само по себе проблемой, так как любой сетевой агент вправе это тело проигнорировать; + * несоблюдение требования симметричности операций `GET` / `PUT` / `DELETE` (т.е., например, после выполнения `DELETE /url` операция `GET /url` продолжает возвращать какие-то данные). + +#### Статус-коды + +Статус-код — это численное машиночитаемое описание результата операции. Все статус-коды делятся на пять больших групп: + * `1xx` — информационные (фактически, какое-то хождение имеет разве что `100 Continue`); + * `2xx` — коды успеха операции; + * `3xx` — коды перенаправлений (индицируют необходимость выполнения дополнительных действий, чтобы считать операцию успешной); + * `4xx` — клиентские ошибки; + * `5xx` — серверные ошибки. + +**NB**: разделение на группы по первой цифре кода имеет очень важное практическое значение. В случае, если возвращаемый сервером код ошибки `xyz` неизвестен клиенту, согласно спецификации клиент обязан выполнить то действие, которое выполнил бы при получении ошибки `x00`. + +В основе технологии статус-кодов лежит понятное желание сделать ошибки машиночитаемыми, так, чтобы все промежуточные агенты могли понять, что конкретно произошло с запросом. Номенклатура статус-кодов HTTP действительно подробно описывает почти любые проблемы, которые могут случиться с HTTP-запросом: недопустимые значения `Accept-*`-заголовков, отсутствующий `Content-Length`, неподдерживаемый HTTP-метод, слишком длинный URI и так далее. + +**NB**: обратите внимание на проблему дизайна спецификации. По умолчанию все `4xx` коды не кэшируются, за исключением: `404`, `405`, `410`, `414`. Мы не сомневаемся, что это было сделано из благих намерений, но подозреваем, что множество людей, знающих об этих тонкостях, примерно совпадает с множеством редакторов спецификации HTTP. + +К сожалению, для описаний ошибок, возникающих в бизнес-логике, номенклатура статус-кодов HTTP совершенно недостаточна и вынуждает использовать статус-коды в нарушение стандарта и/или обогащать ответ дополнительной информацией об ошибке. Проблемы имплементации системы ошибок в HTTP API мы обсудим подробнее в главе «Работа с ошибками в HTTP API». + +#### Важное замечание о кэшировании + +Кэширование — исключительно важная часть любой современной микросервисной архитектуры, и велик соблазн управлять им на уровне протокола — благо, стандарт предоставляет весьма функциональную и продвинутую функциональность работы с кэшами. Однако автор этой книги должен предостеречь читателя: если вы планируете имплементировать такую логику, прочитайте стандарт очень внимательно. Неверное толкование тех или иных параметров кэширования может приводить к крайне неприятным ситуациям; например, в практике автора был случай, когда случайное удаление настроек для определённой географической области (эндпойнт начал возвращать `404`) привело к неработоспособности сервиса на протяжении нескольких часов, поскольку разработчики протокола не учли, что статус `404` по умолчанию кэшируется, и клиенты просто не запрашивают новую версию настроек, пока не истечёт время жизни кэша. + +#### Важное замечание о консистентности + +Один и тот же параметр в разных ситуациях может находиться в разных частях запроса. Скажем, идентификатор партнёра, совершающего запрос, может быть передан: + + * в имени поддомена `{partner_id}.domain.tld`; + * как часть пути `/v1/{partner_id}/orders`; + * как query-параметр `/v1/orders?partner_id=`; + * как заголовок + + ``` + GET /v1/orders HTTP/1.1 + X-ApiName-Partner-Id: + ``` + + * как поле в теле запроса + + ``` + POST /v1/orders/retrieve HTTP/1.1 + + { + "partner_id": + } + ``` + +Возможны и более экзотические варианты: размещение параметра в схеме запроса или `Content-Type` ответа. + +Однако при перемещении параметра между различными составляющими запроса мы столкнёмся с тремя неприятными явлениями: + * некоторые виды параметров чувствительны к регистру (путь, query-параметры, имена полей в JSON), некоторые нет (домен, имена заголовков); + * при этом со *значениями* заголовков и вовсе неразбериха: часть из них по стандарту обязательно нечувствительна к регистру (в частности, `Content-Type`), а часть, напротив, обязательно чувствительна (например, `ETag`); + * наборы допустимых символов и правила экранирования также различны для разных частей запроса + * для path, например, стандарта экранирования символов `/`, `?` и `#` не существует; + * для разных частей запросов используется разный кейсинг: + + * `kebab-case` для домена, заголовков и пути; + * `snake_case` для query-параметров; + * `snake_case` или `camelCase` для тела запроса; + + При этом использование и `snake_case`, и `camelCase` в доменном имени невозможно, так как знак подчеркивания в доменных именах недопустим, а заглавные буквы будут приведены к строчным. + +Чисто теоретически возможно использование `kebab-case` во всех случаях, но в большинстве языков программирования имена переменных и полей объектов в `kebab-case` недопустимы, что приведёт к неудобству работы с таким API. + +Короче говоря, ситуация с кейсингом настолько плоха и запутанна, что консистентного и удобного решения попросту нет. В этой книге мы придерживаемся следующего правила: токены даются в том кейсинге, который является общепринятым для той секции запроса, в которой находится токен; если положение токена меняется, то меняется и кейсинг. (Мы далеки от того, чтобы рекомендовать этот подход всюду; наша общая рекомендация, скорее — не умножать энтропию и пытаться минимизировать такого рода коллизии.) + +**NB**. Вообще говоря, JSON исходно — это JavaScript Object Notation, а в языке JavaScript кейсинг по умолчанию - `camelCase`. Мы, тем не менее, позволим себе утверждать, что JSON давно перестал быть форматом данных, привязанным к JavaScript, и в настоящее время используется для организации взаимодействия агентов, реализованных на любых языках программирования. Использование `snake_case`, по крайней мере, позволяет легко перебрасывать параметр из query в тело и обратно, что, обычно, является наиболее частотным кейсом при разработке HTTP API. Впрочем, обратный вариант (использование `camelCase` в именах query-параметров) тоже допустим. \ No newline at end of file diff --git a/src/ru/drafts/05-Раздел IV. HTTP API и REST/02.md b/src/ru/drafts/05-Раздел IV. HTTP API и REST/02.md deleted file mode 100644 index e69de29..0000000 diff --git a/src/ru/drafts/05-Раздел IV. HTTP API и REST/03.md b/src/ru/drafts/05-Раздел IV. HTTP API и REST/03.md index 1444ef7..e69de29 100644 --- a/src/ru/drafts/05-Раздел IV. HTTP API и REST/03.md +++ b/src/ru/drafts/05-Раздел IV. HTTP API и REST/03.md @@ -1,172 +0,0 @@ -### Составляющие HTTP запросов и их семантика - -Третье важное подготовительное упражнение, которое мы должны сделать — это дать описание формата HTTP-запросов и ответов и прояснить базовые понятия. Многое из написанного ниже может показаться читателю самоочевидным, но, увы, специфика протокола такова, что даже базовые сведения о нём, без которых мы не сможем двигаться дальше, разбросаны по обширной и фрагментированной документации. Ниже мы попытаемся дать структурированный обзор протокола в том объёме, который необходим нам для проектирования HTTP API. - -В описании семантики и формата протокола мы будем руководствоваться свежевышедшим [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html), который заменил аж девять предыдущих спецификаций, описывавших разные аспекты технологии (при этом большое количество различной дополнительной функциональности всё ещё покрывается отдельными стандартами. В частности, принципы HTTP-кэширования описаны в отдельном [RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html), а широко используемый в API метод `PATCH` так и не вошёл в основной RFC и регулируется [RFC 5789](https://www.rfc-editor.org/rfc/rfc5789.html)). - -HTTP-запрос представляет собой применение определённого глагола к URL с указанием версии протокола и передачей дополнительной мета-информации в заголовках и, возможно, каких-то данных в теле запроса: - -``` -POST /v1/orders HTTP/1.1 -Host: our-api-host.tld -Content-Type: application/json - -{ - "coffee_machine_id": 123, - "currency_code": "MNT", - "price": "10.23", - "recipe": "lungo", - "offer_id": 321, - "volume": "800ml" -} -``` - -Ответом на HTTP-запрос будет являться конструкция, состоящая из версии протокола, статус-кода ответа, сообщения, заголовков и, возможно, тела ответа. - -``` -HTTP/1.1 201 Created -Location: /v1/orders/123 -Content-Type: application/json - -{ - "id": 123 -} -``` - -**NB**: в HTTP/2 (и будущем HTTP/3) вместо единого текстового формата используются отдельные бинарные фреймы для передачи заголовков и данных. Этот факт не влияет на излагаемые архитектурные принципы, но во избежание двусмысленности мы будем давать примеры в формате HTTP/1.1. Подробнее о формате HTTP/2 можно прочитать [здесь](https://hpbn.co/http2/). - -##### URL - -URL — единица адресации в HTTP API (некоторые евангелисты технологии даже используют термин «пространство URL» как синоним для Мировой паутины). Предполагается, что API в парадигме REST должно использовать систему адресов столь же гранулярную, как и предметная область; иными словами, у любых сущностей, которыми мы можем манипулировать независимо, должен быть свой URL. - -Формат URL регулируется [отдельным стандартом](https://url.spec.whatwg.org/), который развивает независимое сообщество Web Hypertext Application Technology Working Group (WHATWG). Считается, что концепция URL (вместе с понятием универсального имени ресурса, URN) составляет более общую сущность URI (универсальный идентификатор ресурса). (Разница между URL и URN заключается в том, что URL позволяет *найти* некоторый ресурс в рамках некоторого протокола доступа, в то время как URN — «внутреннее» имя объекта, которое само по себе никак не помогает получить к нему доступ.) - -URL принято раскладывать на составляющие, каждая из которых может отсутствовать: - * схема (scheme) — протокол обращения (в нашем случае всегда `https:`); - * хост (host; домен или IP-адрес), к которому обращён запрос — самая крупная единица адресации; - * домен может включать в себя поддомены; - * порт (port); - * путь (path) — часть URL между доменным именем (с портом) и символами `?`, `#` или концом строки - * путь принято разбивать по символу `/` и работать с каждой частью как отдельным токеном — но стандарт этого, вообще говоря, не предписывает; - * пути с символом `/` и без символа `/` в конце (скажем `/root/leaf` и `/root/leaf/`) с точки зрения стандарта являются разными (и URL, отличающиеся только наличием/отсутствием слэша, считаются разными URL), хотя практически нам неизвестны аргументы в пользу того, чтобы не считать такие пути эквивалентными; - * пути могут содержать секции `.` и/или `..`, которые предлагается трактовать по аналогии с такими же символами в путях на файловой системе (и, соответственно, считать URL `/root/leaf`, `/root/./leaf`, `/root/branch/../leaf` эквивалентными), но стандарт этого вновь не предписывает; - * запрос (query) — часть URL после знака `?` до знака `#` или конца строки; - * query принято раскладывать на пары `ключ=значение`, разделённые символом `&`; следует вновь иметь в виду, что стандарт не предписывает query строго соответствовать этому формату; - * фрагмент (fragment; также якорь, anchor) — часть URL после знака `#`; - * фрагмент традиционно рассматривается как адресация внутри запрошенного документа, поэтому многими агентами опускается при выполнении запроса; - * два URL с различными значениями фрагмента могут считаться одинаковыми — а могут не считаться, зависит от контекста. - -В HTTP-запросах, как правило (но не обязательно) схема, хост и порт опускаются (и считаются совпадающими с параметрами соединения). (Это соглашение, кстати, Филдинг считает самой большой проблемой дизайна протокола.) - -**NB**: в стандарте также перечислены разнообразные исторические наслоения (например, передача логинов и паролей в URL или использование не-UTF кодировки), которые нам в рамках вопросов дизайна API неинтересны. Также стандарт содержит правила сериализации, нормализации и сравнения URL, которые в целом полезно знать разработчику HTTP API. - -##### Заголовки - -Заголовки — это *метаинформация*, привязанная к запросу или ответу. Она может описывать какие-то свойства передаваемых данных (например, `Content-Length`), дополнительные сведения о клиенте или сервере (`User-Agent`, `Date`), или просто содержать поля, не относящиеся непосредственно к смыслу запроса/ответа (например, `Authorization`). - -Важное свойство заголовков — это возможность считывать их до того, как получено тело сообщения. Таким образом, заголовки могут, во-первых, сами по себе влиять на обработку запроса или ответа, и ими можно относительно легко манипулировать при проксировании — и многие сетевые агенты действительно это делают, добавляя или модифицируя заголовки по своему усмотрению (в частности, современные веб-браузеры добавляют к запросам целую коллекцию заголовков: `User-Agent`, `Origin`, `Accept-Language`, `Connection`, `Referer`, `Sec-Fetch-*` и так далее, а современное ПО веб-серверов, в свою очередь, автоматически добавляет или модифицирует такие заголовки как `X-Powered-By`, `Date`, `Content-Length`, `Content-Encoding`, `X-Forwarded-For`). - -Подобное вольное обращение с заголовками создаёт определённые проблемы, если ваш API предусматривает передачу дополнительных полей метаданных, поскольку придуманные вами имена полей могут случайно совпасть с какими-то из существующих стандартных имён (или ещё хуже — в будущем появится новое стандартное поле, совпадающее с вашим). Долгое время во избежание подобных коллизий использовался префикс `X-`; уже более 10 лет как эта практика объявлена устаревшей и не рекомендуется к использованию (см. подробный разбор вопроса в [RFC 6648](https://www.rfc-editor.org/rfc/rfc6648)), однако отказа от этого префикса по факту не произошло (и многие широко распространённые нестандартные заголовки, например, `X-Forwarded-For`, его всё ещё содержат). Таким образом, использование префикса `X-` вероятность коллизий снижает, но не устраняет. Тот же RFC вполне разумно предлагает использовать вместо `X-` префикс в виде имени компании. (Мы со своей стороны склонны рекомендовать использовать оба префикса в формате `X-ApiName-Field`; префикс `X-` для читабельности [чтобы отличать специальные заголовки от стандартных], а префикс с именем компании или API — чтобы не произошло коллизий с каким-нибудь другим нестандартным префиксом.) - -Помимо прочего заголовки используются как управляющие конструкции — это т.н. «content negotiation», т.е. договорённость клиента и сервера о формате ответа (через заголовки `Accept*`) и условные запросы, позволяющие сэкономить трафик на возврате ответа целиком или частично (через заголовки `If-*`-заголовки, такие как `If-Range`, `If-Modified-Since` и так далее). - -##### HTTP-глаголы - -Важнейшая составляющая HTTP запроса — это глагол (метод), описывающий операцию, применяемую к ресурсу. RFC 9110 описывает восемь глаголов (`GET`, `POST`, `PUT`, `DELETE`, `HEAD`, `CONNECT`, `OPTIONS` и `TRACE`), из которых нас как разработчиков API интересует первые четыре. `CONNECT`, `OPTIONS` и `TRACE` — технические методы, которые очень редко используются в HTTP API (за исключением `OPTIONS`, который необходимо реализовать, если необходим доступ к API из браузера). Теоретически, `HEAD` (метод получения *только метаданных*, то есть заголовков, ресурса) мог бы быть весьма полезен в HTTP API, но по неизвестным нам причинам практически в этом смысле не используется. - -Помимо RFC 9110, множество других RFC предлагают использовать дополнительные HTTP-глаголы (такие, например, как `COPY`, `LOCK`, `SEARCH` — полный список можно найти [здесь](http://www.iana.org/assignments/http-methods/http-methods.xhtml)), однако из всего разнообразия предложенных стандартов лишь один имеет широкое хождение — метод `PATCH`. Причины такого положения дел довольно тривиальны — этих пяти методов (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`) достаточно для почти любого HTTP API. - -HTTP-глагол определяет два важных свойства HTTP-вызова: - * его семантику (что представляет собой операция); - * его побочные действия, а именно: - * является ли запрос модифицирующим (и можно ли кэшировать ответ); - * является ли запрос идемпотентным. - -| Глагол | Семантика | Безопасный (немодифицирующий) | Идемпотентный | Имеет тело | -|--------|----------------------------------|---------------|---------------|------------| -| GET | Возвращает представление ресурса | да | да | нет | -| PUT | Заменяет (полностью перезаписывает) ресурс согласно данным, переданным в теле запроса | да | да | да | -| DELETE | Удаляет ресурс | да | да | нет | -| POST | Обрабатывает запрос в соответствии со своим внутренним устройством | да | нет | да | -| PATCH | Модифицирует (частично перезаписывает) ресурс согласно данным, переданным в теле запроса | да | нет | да | - -Важное свойство модифицирующих идемпотентных глаголов — это то, что **URL запроса является его ключом идемпотентности**. `PUT /url` полностью перезаписывает ресурс так, что `GET /url` возвращает представление, соответствующее переданном в `PUT /url` телу (в случае JSON-over-HTTP API это, как правило, просто означает, что `GET /url` возвращает в точности тот же контент, чтобы передан в `PUT /url`, с точностью до значений полей по умолчанию). `DELETE /url` обязан удалить указанный ресурс — так, что `GET /url` должен вернуть `404` или `410`. - -Идемпотентность и симметричность методов `GET` / `PUT` / `DELETE` влечёт за собой запрет `GET` и `DELETE` иметь тело запроса (поскольку этому телу невозможно приписать никакой осмысленной роли). Однако (по-видимому в связи с тем, что многие разработчики попросту не знают семантику этих методов) распространённое ПО веб-серверов обычно разрешает этим методам иметь тело запроса и транслирует его дальше к коду обработки эндпойнта (использование этой практики мы решительно не рекомендуем). - -Достаточно очевидным образом ответы на модифицирующие запросы не кэшируются (хотя при определённых условиях закэшированный ответ метода `POST` может быть использован при последующем `GET`-запросе) и, таким образом, повторный `POST` / `PUT` / `DELETE` / `PATCH` запрос обязательно будет доставлен до конечного сервера (ни один промежуточный агент не имеет права ответить из кэша). В случае `GET`-запроса это, вообще говоря, неверно — гарантией может служить только наличие в ответе директив кэширования `no-store` или `no-cache`. - -Один из самых частых антипаттернов разработки HTTP API — это использование HTTP-глаголов в нарушение их семантики: - * размещение модифицирующих операций за `GET` - * промежуточные агенты могут ответить на такой запрос из кэша, если какая-то из директив кэширования отсутствует, либо, напротив, повторить запрос при получении сетевого таймаута; - * некоторые агенты считают себя вправе переходить по таким ссылкам без явного волеизъявления пользователя или разработчика; например, социальные сети и мессенджеры выполняют такие вызовы для генерации оформления ссылки, если пользователь пытается ей поделиться; - * размещение неидемпотентных операций за идемпотентными методами `PUT` / `DELETE` - * хотя промежуточные агенты редко автоматически повторяют модифицирующие запросы, тем не менее это легко может сделать используемый разработчиком клиента или сервера фреймворк; - * обычно эта ошибка сочетается с наличием у запроса тела (чтобы всё-таки отличать, что конкретно нужно перезаписать или удалить), что является само по себе проблемой, так как любой сетевой агент вправе это тело проигнорировать; - * несоблюдение требования симметричности операций `GET` / `PUT` / `DELETE` (т.е., например, после выполнения `DELETE /url` операция `GET /url` продолжает возвращать какие-то данные). - -#### Статус-коды - -Статус-код — это численное машиночитаемое описание результата операции. Все статус-коды делятся на пять больших групп: - * `1xx` — информационные (фактически, какое-то хождение имеет разве что `100 Continue`); - * `2xx` — коды успеха операции; - * `3xx` — коды перенаправлений (индицируют необходимость выполнения дополнительных действий, чтобы считать операцию успешной); - * `4xx` — клиентские ошибки; - * `5xx` — серверные ошибки. - -**NB**: разделение на группы по первой цифре кода имеет очень важное практическое значение. В случае, если возвращаемый сервером код ошибки `xyz` неизвестен клиенту, согласно спецификации клиент обязан выполнить то действие, которое выполнил бы при получении ошибки `x00`. - -В основе технологии статус-кодов лежит понятное желание сделать ошибки машиночитаемыми, так, чтобы все промежуточные агенты могли понять, что конкретно произошло с запросом. Номенклатура статус-кодов HTTP действительно подробно описывает почти любые проблемы, которые могут случиться с HTTP-запросом: недопустимые значения `Accept-*`-заголовков, отсутствующий `Content-Length`, неподдерживаемый HTTP-метод, слишком длинный URI и так далее. - -**NB**: обратите внимание на проблему дизайна спецификации. По умолчанию все `4xx` коды не кэшируются, за исключением: `404`, `405`, `410`, `414`. Мы не сомневаемся, что это было сделано из благих намерений, но подозреваем, что множество людей, знающих об этих тонкостях, примерно совпадает с множеством редакторов спецификации HTTP. - -К сожалению, для описаний ошибок, возникающих в бизнес-логике, номенклатура статус-кодов HTTP совершенно недостаточна и вынуждает использовать статус-коды в нарушение стандарта и/или обогащать ответ дополнительной информацией об ошибке. Проблемы имплементации системы ошибок в HTTP API мы обсудим подробнее в главе «Работа с ошибками в HTTP API». - -#### Важное замечание о кэшировании - -Кэширование — исключительно важная часть любой современной микросервисной архитектуры, и велик соблазн управлять им на уровне протокола — благо, стандарт предоставляет весьма функциональную и продвинутую функциональность работы с кэшами. Однако автор этой книги должен предостеречь читателя: если вы планируете имплементировать такую логику, прочитайте стандарт очень внимательно. Неверное толкование тех или иных параметров кэширования может приводить к крайне неприятным ситуациям; например, в практике автора был случай, когда случайное удаление настроек для определённой географической области (эндпойнт начал возвращать `404`) привело к неработоспособности сервиса на протяжении нескольких часов, поскольку разработчики протокола не учли, что статус `404` по умолчанию кэшируется, и клиенты просто не запрашивают новую версию настроек, пока не истечёт время жизни кэша. - -#### Важное замечание о консистентности - -Один и тот же параметр в разных ситуациях может находиться в разных частях запроса. Скажем, идентификатор партнёра, совершающего запрос, может быть передан: - - * в имени поддомена `{partner_id}.domain.tld`; - * как часть пути `/v1/{partner_id}/orders`; - * как query-параметр `/v1/orders?partner_id=`; - * как заголовок - - ``` - GET /v1/orders HTTP/1.1 - X-ApiName-Partner-Id: - ``` - - * как поле в теле запроса - - ``` - POST /v1/orders/retrieve HTTP/1.1 - - { - "partner_id": - } - ``` - -Возможны и более экзотические варианты: размещение параметра в схеме запроса или `Content-Type` ответа. - -Однако при перемещении параметра между различными составляющими запроса мы столкнёмся с тремя неприятными явлениями: - * некоторые виды параметров чувствительны к регистру (путь, query-параметры, имена полей в JSON), некоторые нет (домен, имена заголовков); - * при этом со *значениями* заголовков и вовсе неразбериха: часть из них по стандарту обязательно нечувствительна к регистру (в частности, `Content-Type`), а часть, напротив, обязательно чувствительна (например, `ETag`); - * наборы допустимых символов и правила экранирования также различны для разных частей запроса - * для path, например, стандарта экранирования символов `/`, `?` и `#` не существует; - * для разных частей запросов используется разный кейсинг: - - * `kebab-case` для домена, заголовков и пути; - * `snake_case` для query-параметров; - * `snake_case` или `camelCase` для тела запроса; - - При этом использование и `snake_case`, и `camelCase` в доменном имени невозможно, так как знак подчеркивания в доменных именах недопустим, а заглавные буквы будут приведены к строчным. - -Чисто теоретически возможно использование `kebab-case` во всех случаях, но в большинстве языков программирования имена переменных и полей объектов в `kebab-case` недопустимы, что приведёт к неудобству работы с таким API. - -Короче говоря, ситуация с кейсингом настолько плоха и запутанна, что консистентного и удобного решения попросту нет. В этой книге мы придерживаемся следующего правила: токены даются в том кейсинге, который является общепринятым для той секции запроса, в которой находится токен; если положение токена меняется, то меняется и кейсинг. (Мы далеки от того, чтобы рекомендовать этот подход всюду; наша общая рекомендация, скорее — не умножать энтропию и пытаться минимизировать такого рода коллизии.) - -**NB**. Вообще говоря, JSON исходно — это JavaScript Object Notation, а в языке JavaScript кейсинг по умолчанию - `camelCase`. Мы, тем не менее, позволим себе утверждать, что JSON давно перестал быть форматом данных, привязанным к JavaScript, и в настоящее время используется для организации взаимодействия агентов, реализованных на любых языках программирования. Использование `snake_case`, по крайней мере, позволяет легко перебрасывать параметр из query в тело и обратно, что, обычно, является наиболее частотным кейсом при разработке HTTP API. Впрочем, обратный вариант (использование `camelCase` в именах query-параметров) тоже допустим. \ No newline at end of file