mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-05-31 22:09:37 +02:00
HTTP API advantages and disadvantages
This commit is contained in:
parent
e9f6495c0a
commit
8247917f32
@ -41,7 +41,7 @@ A Uniform Resource Locator (URL) is an addressing unit in an HTTP API. Some evan
|
||||
|
||||
The URL format is governed by a [separate standard](https://url.spec.whatwg.org/) developed by an independent body known as the Web Hypertext Application Technology Working Group (WHATWG). The concepts of URLs and Uniform Resource Names (URNs) together constitute a more general entity called Uniform Resource Identifiers (URIs). (The difference between the two is that a URL allows for *locating* a resource within the framework of some protocol whereas a URN is an “internal” entity name that does not provide information on how to find the resource.)
|
||||
|
||||
URLs are decomposed into sub-components, each of which is optional:
|
||||
URLs can be decomposed into sub-components, each of which is optional. While the standard enumerates a number of legacy practices, such as passing logins and passwords in URLs or using non-UTF encodings, we will skip discussing those. Instead, we will focus on the following components that are relevant to the topic of HTTP API design:
|
||||
* A scheme: a protocol to access the resource (in our case it is always `https:`)
|
||||
* A host: a top-level address unit in the form of either a domain name or an IP address. A host might contain subdomains.
|
||||
* A port.
|
||||
@ -58,7 +58,7 @@ URLs are decomposed into sub-components, each of which is optional:
|
||||
|
||||
In HTTP requests, the scheme, host, and port are usually (but not always) omitted and presumed to be equal to the connection parameters. (Fielding actually names this arrangement one of the biggest flaws in the protocol design.)
|
||||
|
||||
**NB**: the standard also enumerates some legacy components such as logins and passwords in URLs or non-UTF encoding marks, which we consider irrelevant to the topic of API design. Additionally, the standard contains rules for serializing, normalizing, and comparing URLs, knowing which can be useful for an HTTP API developer.
|
||||
Additionally, the standard contains rules for serializing, normalizing, and comparing URLs, knowing which can be useful for an HTTP API developer.
|
||||
|
||||
##### Headers
|
||||
|
||||
@ -84,9 +84,9 @@ HTTP verbs define two important characteristics of an HTTP call:
|
||||
|
||||
| Verb | Semantics | Is safe (non-modifying) | Is idempotent | Can have a body |
|
||||
|------|-----------|-------------------------|---------------|------------|
|
||||
| GET | Returns a representation of a resource | Yes | Yes | No |
|
||||
| GET | Returns a representation of a resource | Yes | Yes | Should not |
|
||||
| PUT | Replaces (fully overwrites) a resource with a provided entity | No | Yes | Yes |
|
||||
| DELETE | Deletes a resource | No | Yes | No |
|
||||
| DELETE | Deletes a resource | No | Yes | Should not |
|
||||
| POST | Processes a provided entity according to its internal semantics | No | No | Yes |
|
||||
| PATCH | Modifies (partially overwrites) a resource with a provided entity | No | No | Yes |
|
||||
|
||||
|
@ -1 +1,83 @@
|
||||
### The HTTP API Advantages and Disadvantages
|
||||
### [Advantages and Disadvantages of HTTP APIs][http-api-pros-and-cons]
|
||||
|
||||
After the three introductory chapters dedicated to clarifying terms and concepts (that's the price of the popularity of the technology) the reader might wonder why this dichotomy exists in the first place, i.e., why do some HTTP APIs rely on HTTP semantics while others reject it in favor of custom arrangements? For example, if we consider the [JSON-RPC response format](https://www.jsonrpc.org/specification#response_object), we will quickly notice that it might be replaced with standard HTTP protocol functionality. Instead of this:
|
||||
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id",
|
||||
"error": {
|
||||
"code": -32600,
|
||||
"message": "Invalid request"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
the server could have simply responded with `400 Bad Request`, passing the request identifier as a custom header like `X-OurCoffeeAPI-Request-Id`. Nevertheless, protocol designers decided to introduce their own custom format.
|
||||
|
||||
This situation (not only with JSON-RPC but with essentially every high-level protocol built on top of HTTP) has developed due to various reasons. Some of them are historical (such as the inability to use many HTTP protocol features in early implementations of the `XMLHttpRequest` functionality in web browsers). However, new RPC protocols relying on the bare minimum of HTTP capabilities continue to emerge today.
|
||||
|
||||
To answer this question, we must emphasize a very important distinction between application-level protocols (such as JSON-RPC in our case) and pure HTTP. A `400 BadRequest` is a transparent status for every intermediary network agent but a JSON-RPC custom error is not. Firstly, only a JSON-RPC-enabled client can read it. Secondly, and more importantly, in JSON-RPC, the request status *is not metadata*. In pure HTTP, the details of the operation, such as the method, requested URL, execution status, and request / response headers are readable *without the necessity to parse the entire body*. In most higher-level protocols, including JSON-RPC, this is not the case: even a protocol-enabled client must read a body to retrieve that information.
|
||||
|
||||
How does an API developer benefit from the capability of reading request and response metadata? The modern client-server communication stack, as envisioned by Fielding, is multi-layered. We can enumerate a number of intermediary agents that process network requests and responses:
|
||||
* Frameworks that developers use to write code
|
||||
* Programming language APIs that frameworks are built on, and operating system APIs that compilers / interpreters of these languages rely on
|
||||
* Intermediary proxy servers between a client and a server
|
||||
* Various abstractions used in server programming, including server frameworks, programming languages, and operating systems
|
||||
* Web server software that is typically placed in front of backend handlers
|
||||
* Additional modern microservice-oriented tools such as API gateways and proxies
|
||||
|
||||
The main advantage that following the letter of the HTTP standard offers is the possibility to rely on intermediary agents, from client frameworks to API gateways, to read the request metadata and perform actions based on it. This includes regulating timeouts and retry policies, logging, proxying, and sharding requests, among other things, without the necessity to write additional code to achieve these functionalities.
|
||||
|
||||
If we try to formulate the main principle of designing HTTP APIs, it will be: **you would rather design an API in a way that intermediary agents can read and interpret request and response metadata**.
|
||||
|
||||
Unlike many alternative technologies, which are usually driven by a single large IT company (such as Facebook, Google, or Apache Software Foundation), tools for working with HTTP APIs are developed by many different companies and collaborations. As a result, instead of a single homogeneous but limited toolkit provided by a single vendor, we have a plethora of instruments that work with HTTP APIs. The most notable examples include:
|
||||
* Proxies and gateways (nginx, Envoy, etc.)
|
||||
* Different IDLs (first of all, OpenAPI) and related tools for working with specifications (Redoc, Swagger UI, etc.) and auto-generating code
|
||||
* Programmer-oriented software that allows for convenient development and debugging of API clients (Postman, Insomnia), etc.
|
||||
|
||||
Of course, most of these instruments will work with APIs that utilize other paradigms. However, the ability to read HTTP metadata makes it possible to easily design complex pipelines such as exporting nginx access logs to Prometheus and generating response status code monitoring dashboards in Grafana that work out of the box.
|
||||
|
||||
Additionally, let's emphasize that the HTTP API paradigm is currently the default choice for *public* APIs. Because of the aforementioned reasons, partners can integrate an HTTP API without significant obstacles, regardless of their technological stack. Moreover, the prevalence of the technology lowers the entry barrier and the requirements for the qualification of partners' engineers.
|
||||
|
||||
The main disadvantage of HTTP APIs is that you have to rely on intermediary agents, from client frameworks to API gateways, to read the request metadata and perform actions based on it. This includes regulating timeouts and retry policies, logging, proxying, and sharding requests, among other things, without your consent. Since HTTP-related specifications are complex and the concepts of REST can be challenging to comprehend, and software engineers do not always write perfect code, these intermediary agents (including partners' developers!) will sometimes interpret HTTP metadata *incorrectly*, especially when dealing with exotic and hard-to-implement standards. Usually, one of the stated reasons for developing new RPC frameworks is the desire to make working with the protocol simple and consistent, thereby reducing the likelihood of errors when writing integration code.
|
||||
|
||||
#### The Question of Performance
|
||||
|
||||
When discussing the advantages of alternative technologies such as GraphQL, gRPC, Apache Thrift, etc., the argument of lower performance of JSON-over-HTTP APIs is often presented. Specifically, the following issues with the technology are commonly mentioned:
|
||||
1. The verbosity of the JSON format:
|
||||
* Mandatory field names in every object, even for an array of similar entities
|
||||
* The large proportion of technical symbols (quotes, braces, commas, etc.) and the necessity to escape them in string values
|
||||
2. The common approach of returning a full resource representation on resource retrieval requests, even if the client only needs a subset of the fields
|
||||
3. The lower performance of data serializing and deserializing operations
|
||||
4. The need to introduce additional encoding, such as Base64, to handle binary data
|
||||
5. Performance quirks of the HTTP protocol itself, particularly the inability to serve multiple simultaneous requests through one connection.
|
||||
|
||||
Let's be honest: HTTP APIs do suffer from the listed problems. However, we can confidently say that the impact of these factors is often overestimated. The reason API vendors care little about HTTP API performance is that the actual overhead is not as significant as perceived. Specifically:
|
||||
|
||||
1. Regarding the verbosity of the format, it is important to note that these issues are mainly relevant when compressing algorithms are not utilized. [Comparisons](https://nilsmagnus.github.io/post/proto-json-sizes/) have shown that enabling compression algorithms such as gzip largely reduces the difference in sizes between JSON documents and alternative binary formats (and there are compression algorithms specifically designed for processing text data, such as [brotli](https://datatracker.ietf.org/doc/html/rfc7932)).
|
||||
|
||||
2. If necessary, API designers can customize the list of returned fields in HTTP APIs. It aligns well with both the letter and the spirit of the standard. However, as we already explained to the reader in the “[Partial Updates](#api-patterns-partial-updates)” chapter, trying to minimize traffic by returning only subsets of data is rarely justified in well-designed APIs.
|
||||
|
||||
3. If standard JSON deserializers are used, the overhead compared to binary standards might indeed be significant. However, if this overhead is a real problem, it makes sense to consider alternative JSON serializers such as [simdjson](https://github.com/simdjson/simdjson). Due to their low-level and highly optimized code, simdjson demonstrates impressive throughput which would be suitable for all APIs except some corner cases.
|
||||
|
||||
4. Generally speaking, the HTTP API paradigm implies that binary data (such as images or video files) is served through separate endpoints. Returning binary data in JSON is only necessary when a separate request for the data is a problem from the performance perspective. These situations are virtually non-existent in server-to-server interactions and/or if HTTP/2 or a higher protocol version is used.
|
||||
|
||||
5. The HTTP/1.1 protocol version is indeed a suboptimal solution if request multiplexing is needed. However, alternate approaches to tackling the problem usually rely on… HTTP/2. Of course, an HTTP API can also be served over HTTP/2.
|
||||
|
||||
Let us reiterate once more: JSON-over-HTTP APIs are *indeed* less performative than binary protocols. Nevertheless, we take the liberty to say that for a well-designed API in common subject areas switching to alternative protocols will generate quite a modest profit.
|
||||
|
||||
#### Advantages and Disadvantages of the JSON Format
|
||||
|
||||
It's not hard to notice that most of the claims regarding HTTP API performance are actually not about the HTTP protocol but the JSON format. There is no problem in developing an HTTP API that will utilize any binary format (including, for instance, [Protocol Buffers](https://protobuf.dev/)). Then the difference between a Protobuf-over-HTTP API and a gRPC API would be just about using granular URLs, status codes, request/response headers, and the ensuing (in)ability to use integrated software tools out of the box.
|
||||
|
||||
However, on many occasions (including this book) developers prefer the textual JSON over binary Protobuf (Flatbuffers, Thrift, Avro, etc.) for a very simple reason: JSON is easy to read. First, it's a text format and doesn't require additional decoding. Second, it's self-descriptive, meaning that property names are included. Unlike Protobuf-encoded messages which are basically impossible to read without a `.proto` file, one can make a very good guess what a JSON document is about at a glance. Provided that request metadata in HTTP APIs is readable as well, we ultimately get a communication format that is easy to parse and understand with just our eyes.
|
||||
|
||||
Apart from being human-readable, JSON features another important advantage: it is strictly formal meaning it does not contain any constructs that can be interpreted differently in different architectures (with a possible exception of the sizes of numbers and strings), and the deserialization result is aligned very well with native data structures (i.e., indexed and associative arrays) of almost every programming language. From this point of view, we actually had no other choice when selecting a format for code samples in this book.
|
||||
|
||||
If you happen to design a less general API for a specific subject area, we still recommend the same approach for choosing a format:
|
||||
* Estimate the overhead of preparing and introducing tools to decipher binary protocols versus the overhead of using not the most optimal data transfer protocols.
|
||||
* Make an assessment of what is more important to you: having a quality but restricted in its capabilities set of bundled software or having the possibility of using a broad range of tools that work with HTTP APIs, even though their quality is not that high.
|
||||
* Evaluate the cost of finding developers proficient with the format.
|
@ -41,7 +41,7 @@ URL — единица адресации в HTTP API (некоторые ева
|
||||
|
||||
Формат URL регулируется [отдельным стандартом](https://url.spec.whatwg.org/), который развивает независимое сообщество Web Hypertext Application Technology Working Group (WHATWG). Считается, что концепция URL (вместе с понятием универсального имени ресурса, URN) составляет более общую сущность URI (универсальный идентификатор ресурса). (Разница между URL и URN заключается в том, что URL позволяет *найти* некоторый ресурс в рамках некоторого протокола доступа, в то время как URN — «внутреннее» имя объекта, которое само по себе никак не помогает получить к нему доступ.)
|
||||
|
||||
URL принято раскладывать на составляющие, каждая из которых опциональна:
|
||||
URL принято раскладывать на составляющие, каждая из которых опциональна. В стандарте перечислены разнообразные исторические наслоения (например, передача логинов и паролей в URL или использование не-UTF кодировки), которые мы опустим. В рамках дизайна HTTP API нам интересны следующие компоненты:
|
||||
* схема (scheme) — протокол обращения (в нашем случае всегда `https:`);
|
||||
* хост (host; домен или IP-адрес) — самая крупная единица адресации;
|
||||
* домен может включать в себя поддомены;
|
||||
@ -59,7 +59,7 @@ URL принято раскладывать на составляющие, ка
|
||||
|
||||
В HTTP-запросах, как правило (но не обязательно) схема, хост и порт опускаются (и считаются совпадающими с параметрами соединения). (Это соглашение, кстати, Филдинг считает самой большой проблемой дизайна протокола.)
|
||||
|
||||
**NB**: помимо указанных компонентов в стандарте перечислены разнообразные исторические наслоения (например, передача логинов и паролей в URL или использование не-UTF кодировки), которые нам в рамках вопросов дизайна API неинтересны. Также стандарт содержит правила сериализации, нормализации и сравнения URL, которые в целом полезно знать разработчику HTTP API.
|
||||
Также стандарт содержит правила сериализации, нормализации и сравнения URL, которые в целом полезно знать разработчику HTTP API.
|
||||
|
||||
##### Заголовки
|
||||
|
||||
@ -85,15 +85,15 @@ HTTP-глагол определяет два важных свойства HTTP
|
||||
|
||||
| Глагол | Семантика | Безопасный (немодифицирующий) | Идемпотентный | Может иметь тело |
|
||||
|--------|----------------------------------|---------------|---------------|------------|
|
||||
| GET | Возвращает представление ресурса | да | да | нет |
|
||||
| GET | Возвращает представление ресурса | да | да | не рекомендуется |
|
||||
| PUT | Заменяет (полностью перезаписывает) ресурс согласно данным, переданным в теле запроса | нет | да | да |
|
||||
| DELETE | Удаляет ресурс | нет | да | нет |
|
||||
| DELETE | Удаляет ресурс | нет | да | не рекомендуется |
|
||||
| POST | Обрабатывает запрос в соответствии со своим внутренним устройством | нет | нет | да |
|
||||
| PATCH | Модифицирует (частично перезаписывает) ресурс согласно данным, переданным в теле запроса | нет | нет | да |
|
||||
|
||||
Важное свойство модифицирующих идемпотентных глаголов — это то, что **URL запроса является его ключом идемпотентности**. `PUT /url` полностью перезаписывает ресурс, заданный своим URL (`/url`), и, таким образом, повтор запроса не изменяет ресурс. Аналогично, повторный вызов `DELETE /url` должен оставить систему в том же состоянии (ресурс `/url` удалён). Учитывая, что метод `GET /url` семантически должен вернуть представление целевого ресурса `/url`, то, если этот метод реализован, он должен возвращать консистентное предыдущим `PUT` / `DELETE` представление. Если ресурс был перезаписан через `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` иметь тело запроса (поскольку этому телу невозможно приписать никакой осмысленной роли). Однако (по-видимому в связи с тем, что многие разработчики попросту не знают семантику этих методов) распространённое ПО веб-серверов обычно разрешает этим методам иметь тело запроса и транслирует его дальше к коду обработки эндпойнта (использование этой практики мы решительно не рекомендуем).
|
||||
Идемпотентность и симметричность методов `GET` / `PUT` / `DELETE` влечёт за собой нежелательность для `GET` и `DELETE` запросов иметь тело (поскольку этому телу невозможно приписать никакой осмысленной роли). Однако (по-видимому в связи с тем, что многие разработчики попросту не знают семантику этих методов) распространённое ПО веб-серверов обычно разрешает этим методам иметь тело запроса и транслирует его дальше к коду обработки эндпойнта (использование этой практики мы решительно не рекомендуем).
|
||||
|
||||
Достаточно очевидным образом ответы на модифицирующие запросы не кэшируются (хотя при определённых условиях закэшированный ответ метода `POST` может быть использован при последующем `GET`-запросе) и, таким образом, повторный `POST` / `PUT` / `DELETE` / `PATCH` запрос обязательно будет доставлен до конечного сервера (ни один промежуточный агент не имеет права ответить из кэша). В случае `GET`-запроса это, вообще говоря, неверно — гарантией может служить только наличие в ответе директив кэширования `no-store` или `no-cache`.
|
||||
|
||||
@ -101,7 +101,7 @@ HTTP-глагол определяет два важных свойства HTTP
|
||||
* Размещение модифицирующих операций за `GET`:
|
||||
* промежуточные агенты могут ответить на такой запрос из кэша, если какая-то из директив кэширования отсутствует, либо, напротив, повторить запрос при получении сетевого таймаута;
|
||||
* некоторые агенты считают себя вправе переходить по таким ссылкам без явного волеизъявления пользователя или разработчика; например, социальные сети и мессенджеры выполняют такие вызовы для генерации оформления ссылки, если пользователь пытается ей поделиться.
|
||||
* Размещение неидемпотентных операций за идемпотентными методами `PUT` / `DELETE`. хотя промежуточные агенты редко автоматически повторяют модифицирующие запросы, тем не менее это легко может сделать используемый разработчиком клиента или сервера фреймворк. Обычно эта ошибка сочетается с наличием у `DELETE`-запроса тела (чтобы всё-таки отличать, что конкретно нужно перезаписать или удалить), что является само по себе проблемой, так как любой сетевой агент вправе это тело проигнорировать.
|
||||
* Размещение неидемпотентных операций за идемпотентными методами `PUT` / `DELETE`. Хотя промежуточные агенты редко автоматически повторяют модифицирующие запросы, тем не менее это легко может сделать используемый разработчиком клиента или сервера фреймворк. Обычно эта ошибка сочетается с наличием у `DELETE`-запроса тела (чтобы всё-таки отличать, что конкретно нужно перезаписать или удалить), что является само по себе проблемой, так как любой сетевой агент вправе это тело проигнорировать.
|
||||
* Несоблюдение требования симметричности операций `GET` / `PUT` / `DELETE`:
|
||||
* например, после выполнения `DELETE /url` операция `GET /url` продолжает возвращать какие-то данные или `PUT /url` ориентируется не на URL, а на данные внутри тела запроса для определения сущности, над которой выполняется операция, и, таким образом, `GET /url` никак не может вернуть представление объекта, только что переданного в `PUT /url`.
|
||||
|
||||
@ -167,6 +167,6 @@ HTTP-глагол определяет два важных свойства HTTP
|
||||
|
||||
Чисто теоретически возможно использование `kebab-case` во всех случаях, но в большинстве языков программирования имена переменных и полей объектов в `kebab-case` недопустимы, что приведёт к неудобству работы с таким API.
|
||||
|
||||
Короче говоря, ситуация с кейсингом настолько плоха и запутанна, что консистентного и удобного решения попросту нет. В этой книге мы придерживаемся следующего правила: токены даются в том кейсинге, который является общепринятым для той секции запроса, в которой находится токен; если положение токена меняется, то меняется и кейсинг. (Мы далеки от того, чтобы рекомендовать этот подход всюду; наша общая рекомендация, скорее — не умножать энтропию и пытаться минимизировать такого рода коллизии.)
|
||||
Короче говоря, ситуация с кейсингом настолько плоха и запутана, что консистентного и удобного решения попросту нет. В этой книге мы придерживаемся следующего правила: токены даются в том кейсинге, который является общепринятым для той секции запроса, в которой находится токен; если положение токена меняется, то меняется и кейсинг. (Мы далеки от того, чтобы рекомендовать этот подход всюду; наша общая рекомендация, скорее — не умножать энтропию и пытаться минимизировать такого рода коллизии.)
|
||||
|
||||
**NB**: вообще говоря, JSON исходно — это JavaScript Object Notation, а в языке JavaScript кейсинг по умолчанию - `camelCase`. Мы, тем не менее, позволим себе утверждать, что JSON давно перестал быть форматом данных, привязанным к JavaScript, и в настоящее время используется для организации взаимодействия агентов, реализованных на любых языках программирования. Использование `snake_case`, по крайней мере, позволяет легко перебрасывать параметр из query в тело и обратно, что, обычно, является наиболее частотным кейсом при разработке HTTP API. Впрочем, обратный вариант (использование `camelCase` в именах query-параметров) тоже допустим.
|
@ -1 +1,84 @@
|
||||
### Преимущества и недостатки HTTP API
|
||||
### [Преимущества и недостатки HTTP API][http-api-pros-and-cons]
|
||||
|
||||
После трёх вступительных глав с прояснением основных терминов и понятий (такова, увы, цена популярности технологии) у читателя может возникнуть резонный вопрос — а почему вообще существует такая дихотомия: какие-то API полагаются на стандартную семантику HTTP, а какие-то полностью от неё отказываются в пользу новоизобретённых стандартов. Например, если мы посмотрим на [формат ответа в JSON-RPC](https://www.jsonrpc.org/specification#response_object), то мы обнаружим, что он легко мог бы быть заменён на стандартные средства протокола HTTP. Вместо
|
||||
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id",
|
||||
"error": {
|
||||
"code": -32600,
|
||||
"message": "Invalid request"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
сервер мог бы ответить просто `400 Bad Request` (с передачей идентификатора запроса, ну скажем, в заголовке `X-OurCoffeeAPI-Request-Id`). Тем не менее, разработчики протокола посчитали нужным разработать свой собственный формат.
|
||||
|
||||
Такая ситуация (не только конкретно с JSON-RPC, а почти со всеми высокоуровневыми протоколами поверх HTTP) сложилась по множеству причин, включая разнообразные исторические (например, невозможность использовать многие возможности HTTP из ранних реализаций `XMLHttpRequest` в браузерах). Однако, новые варианты RPC-протоколов, использующих абсолютный минимум возможностей HTTP, продолжают появляться и сегодня.
|
||||
|
||||
Чтобы разрешить этот парадокс, обратим внимание на одно принципиальное различие между использованием протоколов уровня приложения (как в нашем примере с JSON-RPC) и чистого HTTP: если ошибка `400 Bad Request` является прозрачной для практически любого сетевого агента, то ошибка в собственном формате JSON-RPC таковой не является — во-первых, потому что понять её может только агент с поддержкой JSON-RPC, а, во-вторых, что более важно, в JSON-RPC статус запроса *не является метаинформацией*. Протокол HTTP позволяет прочитать такие детали, как метод и URL запроса, статус операции, заголовки запроса и ответа, *не читая тело запроса целиком*. Для большинства протоколов более высокого уровня, включая JSON-RPC, это не так: даже если агент и обладает поддержкой протокола, ему необходимо прочитать и разобрать тело ответа.
|
||||
|
||||
Каким образом эта самая возможность читать метаданные нам может быть полезна? Современный стек взаимодействия между клиентом и сервером является (как и предсказывал Филдинг) многослойным. Мы можем выделить множество агентов разного уровня, которые, так или иначе, обрабатывают сетевые запросы и ответы:
|
||||
* разработчик пишет код поверх какого-то фреймворка, который отправляет запросы;
|
||||
* фреймворк базируется на API языка программирования, компилятор или интерпретатор которого, в свою очередь, полагается на API операционной системы;
|
||||
* запрос доходит до сервера, возможно, через промежуточные HTTP-прокси;
|
||||
* сервер, в свою очередь, тоже представляет собой несколько слоёв абстракции в виде фреймворка, языка программирования и ОС;
|
||||
* перед конечным сервером, как правило, находится веб-сервер, проксирующий запрос, а зачастую и не один;
|
||||
* в современных облачных архитектурах HTTP-запрос, прежде чем дойти до конечного обработчика, пройдёт через несколько абстракций в виде прокси и гейтвеев.
|
||||
|
||||
Главное преимущество, которое предоставляет следование букве стандарта HTTP — возможность положиться на то, что промежуточные агенты, от клиентских фреймворков до API-гейтвеев, умеют читать метаданные запроса и выполнять какие-то действия с их использованием — настраивать политику перезапросов и таймауты, логировать, кэшировать, шардировать, проксировать и так далее — без необходимости писать какой-то дополнительный код.
|
||||
|
||||
Если попытаться сформулировать главный принцип разработки HTTP API, то мы получим примерно следующее: **лучше бы ты разрабатывал API так, чтобы промежуточные агенты могли читать и интерпретировать мета-информацию запроса и ответа**.
|
||||
|
||||
В отличие от большинства альтернативных технологий, основной импульс развития которых исходит от какой-то одной крупной IT-компании (Facebook, Google, Apache Software Foundation), инструменты для HTTP API разрабатываются множеством различных компаний и коллабораций. Соответственно, вместо гомогенного, но ограниченного в возможностях фреймворка, для HTTP API мы имеем множество самых разных инструментов, таких как:
|
||||
* различные прокси и API-гейтвеи (nginx, Envoy);
|
||||
* различные форматы описания спецификаций (в первую очередь, OpenAPI) и связанные инструменты для работы со спецификациями (Redoc, Swagger UI) и кодогенерации;
|
||||
* ПО для разработчиков, позволяющее удобным образом разрабатывать и отлаживать клиенты API (Postman, Insomnia) и так далее.
|
||||
|
||||
Конечно, большинство этих инструментов применимы и для работы с API, реализующими альтернативные парадигмы. Однако именно способность промежуточных агентов считывать метаданные HTTP запросов позволяет легко строить сложные конвейеры типа экспортировать access-логи nginx в Prometheus и из коробки получить удобные мониторинги статус-кодов ответов в Grafana.
|
||||
|
||||
Отдельно заметим что HTTP API является на сегодняшний день выбором по умолчанию при разработке публичных API. В силу озвученных выше причин, как бы ни был устроен технологический стек партнёра, интегрироваться с HTTP API он сможет без особых усилий. При этом распространённость технологии понижает и порог входа, и требования к квалификации инженеров партёра.
|
||||
|
||||
Главным недостатком HTTP API является то, что промежуточные агенты, от клиентских фреймворков до API-гейтвеев, умеют читать метаданные запроса и выполнять какие-то действия с их использованием — настраивать политику перезапросов и таймауты, логировать, кэшировать, шардировать, проксировать и так далее — даже если вы их об этом не просили. Более того, так как стандарты HTTP являются сложными, концепция REST — непонятной, а разработчики программного обеспечения — неидеальными, то промежуточные агенты (и разработчики партнёра!) могут трактовать метаданные запроса *неправильно*. Особенно это касается каких-то экзотических и сложных в имплементации стандартов. Как правило, одной из причин разработки новых RPC-фреймворков декларируется стремление обеспечить простоту и консистентность работы с протоколом, чтобы таким образом уменьшить поле для потенциальных ошибок в реализации интеграции с API.
|
||||
|
||||
#### Вопросы производительности
|
||||
|
||||
В пользу многих современных альтернатив HTTP API — таких как GraphQL, gRPC, Apache Thrift — часто приводят аргумент о низкой производительности JSON-over-HTTP API по сравнению с рассматриваемой технологией; конкретнее, называются следующие проблемы:
|
||||
|
||||
1. Избыточность формата:
|
||||
* в JSON необходимо всякий раз передавать имена всех полей, даже если передаётся массив из большого количества одинаковых объектов;
|
||||
* большое количество технических символов — кавычек, скобок, запятых — и необходимость экранировать служебные символы в строках.
|
||||
2. HTTP API в ответ на запрос к ресурсу возвращает представление ресурса целиком, хотя клиенту могут быть интересны только отдельные поля.
|
||||
3. Низкая производительность операций сериализации и десериализации данных.
|
||||
4. Передача бинарных данных требует дополнительного кодирования, например, через Base64.
|
||||
5. Низкая производительность самого протокола HTTP (в частности, невозможность мультиплексирования нескольких запросов и ответов по одному соединению).
|
||||
|
||||
Будем здесь честны: большинство существующих имплементаций HTTP API действительно страдают от указанных проблем. Тем не менее, мы берём на себя смелость заявить, что все эти проблемы большей частью надуманы, и их решению не уделяют большого внимания потому, что указанные накладные расходы не являются сколько-нибудь заметными для большинства вендоров API. В частности:
|
||||
|
||||
1. Если мы говорим об избыточности формата, то необходимо сделать важную оговорку: всё вышесказанное верно, если мы не применяем сжатие. [Сравнения показывают](https://nilsmagnus.github.io/post/proto-json-sizes/), что использование gzip практически нивелирует разницу в размере JSON документов относительно альтернативных бинарных форматов (а есть ещё и специально предназначенные для текстовых данных архиваторы, например, [brotli](https://datatracker.ietf.org/doc/html/rfc7932)).
|
||||
|
||||
2. Вообще говоря, если такая нужда появляется, то и в рамках HTTP API вполне можно регулировать список возвращаемых полей ответа, это вполне соответствует духу и букве стандарта. Однако, мы должны заметить, что экономия трафика на возврате частичных состояний (которую мы рассматривали подробно в главе «[Частичные обновления](#api-patterns-partial-updates)») очень редко бывает оправдана.
|
||||
|
||||
3. Если использовать стандартные десериализаторы JSON, разница по сравнению с бинарными форматами может оказаться действительно очень большой. Если, однако, эти накладные расходы являются проблемой, стоит обратиться к альтернативным десериализаторам — в частности, [simdjson](https://github.com/simdjson/simdjson). Благодаря оптимизированному низкоуровневому коду simdjson показывает отличную производительность, которой может не хватить только совсем уж экзотическим API.
|
||||
|
||||
4. Вообще говоря, парадигма HTTP API подразумевает, что для бинарных данных (такие как изображения или видеофайлы) предоставляются отдельные эндпойнты. Передача бинарных данных в теле JSON-ответа необходима только в случаях, когда отдельный запрос за ними представляет собой проблему с точки зрения производительности. Такой проблемы фактически не существует в server-2-server взаимодействии и в протоколе HTTP 2.0 и выше.
|
||||
|
||||
5. Протокол HTTP 1.1 действительно неидеален с точки зрения мультиплексирования запросов. Однако альтернативные парадигмы организации API для решения этой проблемы опираются… на HTTP версии 2.0. Разумеется, и HTTP API можно построить поверх версии 2.0.
|
||||
|
||||
На всякий случай ещё раз уточним: JSON-over-HTTP API *действительно* проигрывает с точки зрения производительности современным бинарным протоколам. Мы, однако, берём на себя смелость утверждать, что производительность хорошо спроектированного и оптимизированного HTTP API достаточна для абсолютного большинства предметных областей, и реальный выигрыш от перехода на альтернативные протоколы окажется незначителен.
|
||||
|
||||
#### Преимущества и недостатки формата JSON
|
||||
|
||||
Как нетрудно заметить, большинство претензий, предъявляемых к концепции HTTP API, относятся вовсе не к HTTP, а к использованию формата JSON. В самом деле, ничто не мешает разработать API, которое будет использовать любой бинарный формат вместо JSON (включая те же [Protocol Buffers](https://protobuf.dev/)), и тогда разница между Protobuf-over-HTTP API и gRPC сведётся только к (не)использованию подробных URL, статус-кодов и заголовков запросов и ответов (и вытекающей отсюда (не)возможности использовать то или иное стандартное программное обеспечение «из коробки»).
|
||||
|
||||
Однако, во многих случаях (включая настоящую книгу) разработчики предпочитают текстовый JSON бинарным Protobuf (Flatbuffers, Thrift, Avro и т.д.) по очень простой причине: JSON очень легко и удобно читать. Во-первых, он текстовый и не требует дополнительной расшифровки; во-вторых, имена полей включены в сам файл. Если сообщение в формате protobuf невозможно прочитать без `.proto`-файла, то по JSON-документу почти всегда можно попытаться понять, что за данные в нём описаны. В совокупности с тем, что при разработке HTTP API мы также стараемся следовать стандартной семантике всей остальной обвязки, в итоге мы получаем API, запросы и ответы к которому (по крайней мере в теории) удобно читаются и интуитивно понятны.
|
||||
|
||||
Помимо человекочитаемости у JSON есть ещё одно важное преимущество: он максимально формален. В нём нет никаких конструкций, которые могут быть по-разном истолкованы в разных архитектурах (с точностью до ограничений на длины чисел и строк), и при этом он удобно ложится в нативные структуры данных (индексные и ассоциативные массивы) почти любого языка программирования. С этой точки зрения у нас фактически не было никакого другого выбора, какой ещё формат данных мы могли бы использовать при написании примеров кода для этой книги.
|
||||
|
||||
В случае же разработки API менее общего назначения, мы рекомендуем подходить к выбору формата по тому же принципу:
|
||||
* взвесить накладные расходы на подготовку и внедрение инструментов чтения бинарных форматов и расшифровки бинарных протоколов против накладных расходов на неоптимальную передачу данных;
|
||||
* оценить, хватит ли вам качественного, но ограниченного в возможностях набора программного обеспечения, идущего в комплекте с альтернативным форматом, или вам важнее возможность использовать широкий спектр инструментов, умеющих работать с HTTP API, пусть они и не всегда высокого качества;
|
||||
* прикинуть, насколько дорого вам обойдутся разработчики, способные работать с этим форматом.
|
@ -1,79 +0,0 @@
|
||||
### Преимущества и недостатки HTTP API
|
||||
|
||||
Итак, после трёх вступительных глав с прояснением основных терминов и понятий (такова, увы, цена популярности технологии) мы перейдём, наконец, непосредственно к вопросам дизайна HTTP API.
|
||||
|
||||
У читателя может возникнуть резонный вопрос — а почему вообще существует такая дихотомия: какие-то API полагаются на стандартную семантику HTTP, а какие-то полностью от неё отказываются в пользу новоизобретённых стандартов. Например, если мы посмотрим на [формат ответа в JSON-RPC](https://www.jsonrpc.org/specification#response_object), то мы обнаружим, что он легко мог бы быть заменён на стандартные средства протокола HTTP. Вместо
|
||||
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id",
|
||||
"error": {
|
||||
"code": -32600,
|
||||
"message": "Invalid request"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
сервер мог бы ответить просто `400 Bad Request` (с передачей идентификатора запроса, ну скажем, в заголовке `X-Request-Id`). Тем не менее, разработчики протокола посчитали нужным разработать свой собственный формат.
|
||||
|
||||
Такая ситуация (не только конкретно с JSON-RPC, а почти со всеми высокоуровневыми протоколами поверх HTTP) сложилась по множеству причин, включая разнообразные исторические (например, невозможность использовать многие возможности HTTP из ранних реализаций `XMLHttpRequest` в браузерах). Однако, нужно отметить одно принципиальное различие между использованием протоколов уровня приложения (как в нашем примере с JSON-RPC) и чистого HTTP: если ошибка `400 Bad Request` является прозрачной для практически любого сетевого агента, то ошибка в собственном формате JSON-RPC таковой не является — во-первых, потому что понять её может только агент с поддержкой JSON-RPC, а, во-вторых, что более важно, в JSON-RPC статус запроса *не является метаинформацией*. Протокол HTTP позволяет прочитать такие детали, как метод и URL запроса, статус операции, заголовки запроса и ответа, *не читая тело запроса целиком*. Для большинства протоколов более высокого уровня, включая JSON-RPC, это не так: даже если агент и обладает поддержкой протокола, ему необходимо прочитать и разобрать тело ответа.
|
||||
|
||||
Каким образом эта самая возможность читать метаданные нам может быть полезна? Современный стек взаимодействия между клиентом и сервером является (как и предсказывал Филдинг) многослойным. Мы можем выделить множество агентов разного уровня, которые, так или иначе, обрабатывают сетевые запросы и ответы:
|
||||
* разработчик пишет код поверх какого-то фреймворка, который отправляет запросы;
|
||||
* фреймворк базируется на API языка программирования, которое, в свою очередь, обращается к API операционной системы;
|
||||
* запрос доходит до сервера, возможно, через промежуточные HTTP-прокси;
|
||||
* сервер, в свою очередь, тоже представляет собой несколько слоёв абстракции в виде фреймворка, языка программирования и ОС;
|
||||
* перед конечным сервером, как правило, находится веб-сервер, проксирующий запрос, а зачастую и не один;
|
||||
* в современных облачных архитектурах HTTP-запрос, прежде чем дойти до конечного обработчика, пройдёт через несколько абстракций в виде прокси и гейтвеев.
|
||||
|
||||
Главное преимущество, которое предоставляет следование букве стандарта HTTP — возможность положиться на то, что промежуточные агенты, от клиентских фреймворков до API-гейтвеев, умеют читать метаданные запроса и выполнять какие-то действия с их использованием — настраивать политику перезапросов и таймауты, логировать, кэшировать, шардировать, проксировать и так далее — без необходимости писать какой-то дополнительный код.
|
||||
|
||||
Если попытаться сформулировать главный принцип разработки HTTP API, то мы получим примерно следующее: **лучше бы ты разрабатывал API так, чтобы промежуточные агенты могли читать и интерпретировать мета-информацию запроса и ответа**.
|
||||
|
||||
Помимо прозрачности протокола есть и некоторые семантические отличия HTTP API от RPC-фреймворков. При разработке HTTP API, как правило, стремятся к тому, чтобы сигнатура `ГЛАГОЛ /url` была максимально читабельна и соответствовала семантике операции над ресурсом, т.е. фактически была эквивалента вызову операции `ГЛАГОЛ` на некоторой инстанции статического объекта `/url`. (RPC-парадигма, напротив, подразумевает, что URL — это имя функции, которая вызывается с передачей параметров действия в query/теле запроса.) Отсюда проистекает популярное самопровозглашённое требование к URL в рамках HTTP API быть существительными, т.к. фактически они эквивалентны переменным в коде. (Мы не видим в этом требовании большого смысла, и не придерживаемся его в этой книге строго — как и в случае имён переменных, нам требуется в первую очередь удобство и понятность, а не бездумное следование правилу. Однако принцип максимизации читабельности конструкции `ГЛАГОЛ /url` мы, разумеется, всячески приветствуем.)
|
||||
|
||||
В отличие от большинства альтернативных технологий, основной импульс развития которых исходит от какой-то одной крупной IT-компании (Facebook, Google, Apache Software Foundation), инструменты для HTTP API разрабатываются множеством различных компаний и коллабораций. Соответственно, вместо гомогенного, но ограниченного в возможностях фреймворка, для HTTP API мы имеем множество самых разных инструментов, таких как:
|
||||
* различные прокси и API-гейтвеи (nginx, Envoy);
|
||||
* различные форматы описания спецификаций (в первую очередь, OpenAPI) и связанные инструменты для работы со спецификациями (Redoc, Swagger UI) и кодогенерации;
|
||||
* ПО для разработчиков, позволяющее удобным образом разрабатывать и отлаживать клиенты API (Postman, Insomnia) и так далее.
|
||||
|
||||
Конечно, большинство этих инструментов применимы и для работы с API, реализующими альтернативные парадигмы. Однако именно способность промежуточных агентов считывать метаданные HTTP запросов позволяет легко строить сложные конвейеры типа экспортировать access-логи nginx в prometheus и из коробки получить удобные мониторинги статус-кодов ответов в Grafana.
|
||||
|
||||
Главным недостатком HTTP API является то, что промежуточные агенты, от клиентских фреймворков до API-гейтвеев, умеют читать метаданные запроса и выполнять какие-то действия с их использованием — настраивать политику перезапросов и таймауты, логировать, кэшировать, шардировать, проксировать и так далее — даже если вы их об этом не просили. Более того, так как стандарты HTTP являются сложными, концепция REST — непонятной, а разработчики программного обеспечения — неидеальными, то промежуточные агенты могут трактовать метаданные запроса *неправильно*. Особенно это касается каких-то экзотических и сложных в имплементации стандартов.
|
||||
|
||||
#### Вопросы производительности
|
||||
|
||||
В пользу многих современных альтернатив HTTP API — таких как GraphQL, gRPC, Apache Thrift — часто приводят аргумент о низкой производительности JSON-over-HTTP API по сравнению с рассматриваемой технологией; конкретнее, называются следующие проблемы:
|
||||
|
||||
1. Избыточность формата:
|
||||
* в JSON необходимо всякий раз передавать имена всех полей, даже если передаётся массив из большого количества одинаковых объектов;
|
||||
* большое количество технических символов — кавычек, скобок, запятых — и необходимость экранировать служебные символы в строках.
|
||||
2. HTTP API в ответ на запрос к ресурсу возвращает представление ресурса целиком, хотя клиенту могут быть интересны только отдельные поля.
|
||||
3. Низкая производительность операций сериализации и десериализации данных.
|
||||
4. Передача бинарных данных требует дополнительного кодирования, например, через Base64.
|
||||
5. Низкая производительность самого протокола HTTP (в частности, невозможность мультиплексирования нескольких запросов и ответов по одному соединению).
|
||||
|
||||
Будем здесь честны: большинство существующих имплементаций HTTP API действительно страдают от указанных проблем. Тем не менее, мы берём на себя смелость заявить, что все эти проблемы большей частью надуманы, и их решению не уделяют большого внимания потому, что указанные накладные расходы не являются сколько-нибудь заметными для большинства вендоров API. В частности:
|
||||
|
||||
1. Если мы говорим об избыточности формата, то необходимо сделать важную оговорку: всё вышесказанное верно, если мы не применяем сжатие. [Сравнения показывают](https://nilsmagnus.github.io/post/proto-json-sizes/), что использование gzip практически нивелирует разницу в размере JSON документов относительно альтернативных бинарных форматов (а есть ещё и специально предназначенные для текстовых данных архиваторы, например, brotli).
|
||||
|
||||
2. Вообще говоря, если такая нужда появляется, то и в рамках HTTP API вполне можно регулировать список возвращаемых полей ответа, это вполне соответствует духу и букве стандарта. Однако, мы должны заметить, что экономия трафика на возврате частичных состояний (которую мы рассматривали подробно в главе «Частичные обновления») очень редко бывает оправдана.
|
||||
|
||||
3. Если использовать стандартные десериализаторы JSON, разница по сравнению с бинарными форматами может оказаться действительно очень большой. Если, однако, эти накладные расходы являются проблемой, стоит обратиться к альтернативным десериализаторам — в частности, [simdjson](https://github.com/simdjson/simdjson). Благодаря оптимизированному низкоуровневому коду simdjson показывает отличную производительность, которой может не хватить только совсем уж экзотическим API.
|
||||
|
||||
4. Вообще говоря, парадигма HTTP API подразумевает, что для бинарных данных (такие как изображения или видеофайлы) предоставляются отдельные эндпойнты. Передача бинарных данных в теле JSON-ответа необходима только в случаях, когда отдельный запрос за ними представляет собой проблему с точки зрения производительности. Такой проблемы фактически не существует в server-2-server взаимодействии и в протоколе HTTP 2.0 и выше.
|
||||
|
||||
5. Протокол HTTP 1.1 действительно неидеален с точки зрения мультиплексирования запросов. Однако альтернативные парадигмы организации API для решения этой проблемы опираются… на HTTP версии 2.0. Разумеется, и HTTP API можно построить поверх версии 2.0.
|
||||
|
||||
На всякий случай ещё раз уточним: JSON-over-HTTP API *действительно* проигрывает с точки зрения производительности современным бинарным протоколам. Мы, однако, берём на себя смелость утверждать, что производительность хорошо спроектированного и оптимизированного HTTP API достаточна для абсолютного большинства предметных областей, и реальный выигрыш от перехода на альтернативные протоколы окажется незначителен.
|
||||
|
||||
#### Преимущества и недостатки формата JSON
|
||||
|
||||
Как нетрудно заметить, большинство претензий, предъявляемых к концепции HTTP API, относятся вовсе не к HTTP, а к использованию формата JSON. В самом деле, ничто не мешает разработать API, которое будет использовать любой бинарный формат вместо JSON (включая тот же Protobuf), и тогда разница между Protobuf-over-HTTP API и gRPC сведётся только к (не)использованию подробных URL, статус-кодов и заголовков запросов и ответов (и вытекающей отсюда (не)возможности использовать то или иное стандартное программное обеспечение «из коробки»).
|
||||
|
||||
Однако, во многих случаях (включая настоящую книгу) разработчики предпочитают текстовый JSON бинарным Protobuf (Flatbuffers, Thrift, Avro и т.д.) по очень простой причине: JSON очень легко и удобно читать. Во-первых, он текстовый и не требует дополнительной расшифровки; во-вторых, имена полей включены в сам файл. Если сообщение в формате protobuf невозможно прочитать без .proto-файла, то по JSON-документу почти всегда можно попытаться понять, что за данные в нём описаны. В совокупности с тем, что при разработке HTTP API мы также стараемся следовать стандартной семантике всей остальной обвязки, в итоге мы получаем API, запросы и ответы к которому (по крайней мере в теории) удобно читаются и интуитивно понятны.
|
||||
|
||||
Помимо человекочитаемости у JSON есть ещё одно важное преимущество: он максимально формален. В нём нет никаких конструкций, которые могут быть по-разном истолкованы в разных архитектурах (с точностью до ограничений на длины чисел и строк), и при этом он удобно ложится в нативные структуры данных почти любого языка программирования. С этой точки зрения у нас фактически не было никакого другого выбора, какой ещё формат данных мы могли бы использовать при написании примеров кода для этой книги. В случае же разработки API менее общего назначения, мы рекомендуем подходить к выбору формата по тому же принципу: взвесить накладные расходы на подготовку и внедрение инструментов чтения бинарных форматов и расшифровки бинарных протоколов против накладных расходов на неоптимальную передачу данных и использование разнородного ПО при разработке API.
|
Loading…
x
Reference in New Issue
Block a user