mirror of
https://github.com/twirl/The-API-Book.git
synced 2025-05-31 22:09:37 +02:00
Bidirectional Data Flows
This commit is contained in:
parent
43c66dd310
commit
4fef3e3575
@ -66,4 +66,75 @@ In fact, the most convenient way of organizing message delivery from the public
|
||||
|
||||
#### Delivering Backend-to-Backend Notifications
|
||||
|
||||
Unlike client applications, server-side messaging implementations stick to a single approach to implement bidirectional data flow
|
||||
Unlike client applications, server-side integrations are universally utilizing a single approach to implementing a bidirectional data flow [apart from polling, which is as applicable to server-to-server integrations as to client-server ones, and bears the same pros and cons] — a separate communication channel for callbacks. In case of public APIs, the dominating practice is using callback URLs (aka “webhooks”).
|
||||
|
||||
Though long polling, WebSocket, MQTT, and HTTP/2 Push technologies are as well applicable to realizing backend-to-backend communication, we find it difficult to name a popular API that utilizes any of them. We assume that the reasons for this are:
|
||||
* Lesser susceptibility to performance issues (servers rarely hit any limits on network bandwidth, and keeping an open connection is not a problem as well)
|
||||
* A broad choice of ready-to-use components to develop a *webhook* service (as it's basically a regular webserver)
|
||||
* The possibility to have a specification covering the communication, and use the advantages of code-generation.
|
||||
|
||||
To integrate via a *webhook*, a partner specifies a URL of their own message processing server, and the API provider calls this endpoint to notify about status changes.
|
||||
|
||||
Let us imagine that in our coffee example the partner has a backend capable of processing newly created orders to be processed by partner's coffee shops, and we need to organize such communication. Realizing this task comprise several steps:
|
||||
|
||||
##### Negotiate a Contract
|
||||
|
||||
Depending on how important the partner is for our business, different options are possible:
|
||||
* The API vendor might develop the functionality of calling the partner's *webhook* utilizing a protocol proposed by the partner
|
||||
* Contrary to the previous, it's partner's job to develop an endpoint to support a format proposed by the API developers
|
||||
* Any combination of the above
|
||||
|
||||
What is important is that the *must* be a formal contract (preferably in a form of a specification) for *webhook*'s request and response formats and all the errors that might happen.
|
||||
|
||||
##### Agree on Authorization and Authentication Methods
|
||||
|
||||
As a *webhook* is a callback channel, you will need to develop a separate authorization system to deal with it as it's *partners* duty to check that the request is genuinely coming from the API backend, not vice versa. We reiterate here our strictest recommendation to stick to existing standard techniques, for example, [mTLS](https://en.wikipedia.org/wiki/Mutual_authentication#mTLS); though in the real world, you will likely have to use archaic methods like fixing the caller server's IP address.
|
||||
|
||||
##### Develop an Interface for Setting the URL of a *Webhook*
|
||||
|
||||
As the callback endpoint is developed by partners, we do not know its URL beforehand. It implies some interface must exist for setting this URL and authorized public keys (probably in a form of a control panel for partners).
|
||||
|
||||
**Importantly**, the operation of setting a *webhook* URL is to be treated as a potentially hazardous one. It is highly desirable to request a second authentication factor to authorize the operations as a potential attacker wreak a lot of havoc if there is a vulnerability in the procedure:
|
||||
* By setting an arbitrary URL, the perpetrator might get access to all partner's orders (and the partner might lose access)
|
||||
* This vulnerability might be used for organizing DoS attacks on third parties
|
||||
* If an internal URL might be set as a *webhook*, a [SSRF attack](https://en.wikipedia.org/wiki/SSRF) might be directed toward the API vendor's own infrastructure.
|
||||
|
||||
#### Typical Problems of *Webhook*-Powered Integrations
|
||||
|
||||
Bidirectional data flows (both client-server and server-server ones, though the latter to a greater extent) bear quite undesirable risks for an API provider. In general, the quality of integration primarily depends on the API developers. In the callback-based integration, it's vice versa: the integration quality depends on how partners implemented the *webhook*. We might face numerous problems with the partners' code:
|
||||
* *Webhook* might return false-positive responses meaning the notification was not actually processed but the success status was returned by the partner's server
|
||||
* On other hand, false-negative responses are also possible if the operation was actually accepted but erroneously returned an error (or just responded in invalid format)
|
||||
* *Webhook* might be processing incoming requests very slowly — up to a point when the requesting server will be just unable to deliver subsequent messages on time
|
||||
* Partner's developers might make a mistake in implementing the idempotency policies, and repeated requests to the *webhook* will lead to errors or data inconsistency on the partner's side
|
||||
* The size of the message body might exceed the limit set in the partner's webserver configuration
|
||||
* On the partner's side, authentication token checking might be missing or flawed so some malefactor might be able to issue requests pretending they come from the genuine API server
|
||||
* Finally, the endpoint might simply be unavailable because of many reasons, starting from technical issues in the data center where partner's servers are located and ending with a human error in setting *webhook*'s URL.
|
||||
|
||||
Obviously, we can't guarantee partners don't make any of these mistakes. The only thing we *can* do is to minimize the impact radius:
|
||||
|
||||
1. The system state must be restorable. If the partner erroneously responded that messages are processed while they are not, there must be a possibility for them to redeem themselves and get the list of missed events and/or the full system state and fix all the issues
|
||||
2. Help partners to write proper code by describing in the documentation all unobvious subtleties that inexperienced developers might be unaware of:
|
||||
* idempotency keys for every operation
|
||||
* delivery guarantees (“at least once,” “exactly ones,” etc.; see the [reference description](https://docs.confluent.io/kafka/design/delivery-semantics.html) on the example of Apache Kafka API)
|
||||
* possibility of the server generating parallel requests and the maximum number of such requests at a time
|
||||
* guarantees of message ordering (i.e., the notifications are always delivered ordered from the oldest one to the newest one) or the absence of such guarantees
|
||||
* the sizes of all messages and message fields in bytes
|
||||
* the retry policy in case an error is returned by the partner's server
|
||||
3. Implement a monitoring system to check the health of partners' endpoints:
|
||||
* if a large number of errors or timeouts occurs, it must be escalated (including notifying the partner about the problem), probably with several escalation tiers
|
||||
* if too many un-processed notifications are stuck, there must be a mechanism of controllable degradation (limiting the number of requests toward the partner, e.g. cutting the demand by disallowing some users to make an order) up to fully disconnecting the partner from the platform.
|
||||
|
||||
#### Message Queues
|
||||
|
||||
As for internal APIs, the *webhook* technology (i.e., the possibility to programmatically define a callback URL) is either not needed at all or is replaced with the [Service Discovery](https://en.wikipedia.org/wiki/Web_Services_Discovery) protocol as services comprising a single backend are symmetrically able to call each other. However, the problems of callback-based integration discussed above are equally actual for internal calls. Requesting an internal API might result in a false-negative mistake, internal clients might be unaware that ordering is not guaranteed, etc.
|
||||
|
||||
To solve these problems, and also to ensure better horizontal scalability, [message queues](https://en.wikipedia.org/wiki/Message_queue) were developed, most notably numerous pub/sub pattern implementations. At present moment, pub/sub-based architectures are very popular in enterprise software development, up to switch any inter-service communication to message queues.
|
||||
|
||||
**NB**: let us note that everything comes with a price, and these delivery guarantees and horizontal scalability are not an exclusion:
|
||||
* all communication becomes eventually consistent with all the implications
|
||||
* decent horizontal scalability and cheap message queue usage are only achievable with at least once/at most once policies and no ordering guarantee
|
||||
* queues might accumulate unprocessed events, introducing increasing delays, and solving this issue on the subscriber's side might be quite non-trivial.
|
||||
|
||||
Also, in public APIs both technologies are frequently used in conjunction: the API backend sends a task to call the *webhook* in the form of publishing an event which the specially designed internal service will try to process by making the call.
|
||||
|
||||
Theoretically, we can imagine an integration that exposes directly accessible message queues in one of the standard formats for partners to subscribe. However, we are unaware of any examples of such APIs.
|
@ -303,7 +303,6 @@ POST /v1/users
|
||||
|
||||
То же соображение применимо и к квотам: партнёры должны иметь доступ к информации о том, какую долю доступных ресурсов они выбрали, и ошибки в случае превышения квоты должны быть информативными.
|
||||
|
||||
|
||||
##### Любые запросы должны быть лимитированы
|
||||
|
||||
Ограничения должны быть не только на размеры полей, но и на размеры списков или агрегируемых интервалов.
|
||||
@ -312,6 +311,14 @@ POST /v1/users
|
||||
|
||||
**Хорошо**: `getOrders({ limit, parameters })` — должно существовать ограничение сверху на размер обрабатываемых и возвращаемых данных и, соответственно, возможность уточнить запрос, если партнёру всё-таки требуется большее количество данных, чем разрешено обрабатывать в одном запросе.
|
||||
|
||||
##### Описывайте политику перезапросов
|
||||
|
||||
Одна из самых больших проблем с точки зрения производительности, с которой сталкивается почти любой разработчик API, и внутренних, и публичных — это отказ в обслуживании вследствие лавины перезапросов: временные проблемы на бэкенде API (например, повышение времени ответа) могут привести к полной неработоспособности сервера, если клиенты начнут очень быстро повторять запрос, не получив или не дождавшись ответа, сгенерировав, таким образом, кратно большую нагрузку в короткий срок.
|
||||
|
||||
Лучшая практика в такой ситуации — это требовать, чтобы клиенты перезапрашивали эндпойнты API с увеличивающимся интервалом (скажем, перевый перезапрос происходит через одну секунду, второй — через две, третий через четыре, и так далее, но не больше одной минуты). Конечно, в случае публичного API такое требование никто не обязан соблюдать, но и хуже от его наличия вам точно не станет: хотя бы часть партнёров прочитает документацию и последует вашим рекомендациям.
|
||||
|
||||
Кроме того, вы можете разработать референсную реализацию политики перезапросов в ваших публичных SDK и следить за правильностью имплементации open-source модулей к вашему API.
|
||||
|
||||
##### Считайте трафик
|
||||
|
||||
В современном мире такой ресурс, как объём переданного трафика, считать уже почти не принято — считается, что Интернет всюду практически безлимитен. Однако он всё-таки не абсолютно безлимитен: всегда можно спроектировать систему так, что объём трафика окажется некомфортным даже и для современных сетей.
|
||||
|
@ -12,7 +12,7 @@ const pendingOrders = await api
|
||||
}, …]}
|
||||
```
|
||||
|
||||
Внимательный читатель может подметить, что этот интерфейс нарушает нашу же рекомендацию, данную в главе «Описание конечных интерфейсов»: количество возвращаемых данных в любом ответе должно быть ограничено, но в нашем интерфейсе отсутствуют какие-либо лимиты. Эта проблема существовала и в предыдущих версиях этого эндпойнта, но отказ от синхронного создания заказа её усугубил: операция создания задания должна работать максимально быстро, и, следовательно, почти все проверки лимитов мы должны проводить асинхронно — а значит, клиент потенциально может создать очень много заданий, что может многократно увеличить размер ответа функции `getOngoingOrders`.
|
||||
Внимательный читатель может подметить, что этот интерфейс нарушает нашу же рекомендацию, данную в главе [«Описание конечных интерфейсов»](#api-design-describing-interfaces): количество возвращаемых данных в любом ответе должно быть ограничено, но в нашем интерфейсе отсутствуют какие-либо лимиты. Эта проблема существовала и в предыдущих версиях этого эндпойнта, но отказ от синхронного создания заказа её усугубил: операция создания задания должна работать максимально быстро, и, следовательно, почти все проверки лимитов мы должны проводить асинхронно — а значит, клиент потенциально может создать очень много заданий, что может многократно увеличить размер ответа функции `getOngoingOrders`.
|
||||
|
||||
**NB**: конечно, не иметь *вообще никакого* ограничения на создание заданий — не самое мудрое решение; какие-то легковесные проверки лимитов должны быть в API. Тем не менее, в рамках этой главы мы фокусируемся именно на проблеме размера ответа сервера.
|
||||
|
||||
@ -223,7 +223,7 @@ GET /v1/partners/{id}/offers/history⮠
|
||||
|
||||
Первый формат запроса позволяет решить задачу (1), т.е. получить все элементы списка, появившиеся позднее последнего известного; второй формат — задачу (2), т.е. перебрать нужно количество записей в истории запросов. Важно, что первый запрос при этом ещё и кэшируемый.
|
||||
|
||||
**NB**: отметим, что в главе [«Описание конечных интерфейсов»](#api-design-describing-interfaces) мы давали рекомендацию не давать доступ во внешнем API к инкрементальным id. Однако, схема этого и не требует: внешние идентификаторы могут быть произвольными (не обязательно монотонными) — достаточно, чтобы они однозначно конвертировались во внутренние монотонные идентификаторы.
|
||||
**NB**: отметим, что в главе [«Описание конечных интерфейсов»](#api-design-describing-interfaces) мы давали рекомендацию не предоставлять доступ во внешнем API к инкрементальным id. Однако, схема этого и не требует: внешние идентификаторы могут быть произвольными (не обязательно монотонными) — достаточно, чтобы они однозначно конвертировались во внутренние монотонные идентификаторы.
|
||||
|
||||
Другим способом организации такого перебора может быть дата создания записи, но этот способ чуть сложнее в имплементации:
|
||||
* дата создания двух записей может полностью совпадать, особенно если записи могут массово генерироваться программно; в худшем случае может получиться так, что в один момент времени было создано больше записей, чем максимальный лимит их извлечения, и тогда часть записей вообще нельзя будет перебрать;
|
||||
|
@ -65,9 +65,12 @@ GET /v1/orders/created-history⮠
|
||||
|
||||
#### Доставка сообщений backend-to-backend
|
||||
|
||||
В отличие от клиентских приложений, серверные API практически безальтернативно используют единственный подход для организации двустороннего взаимодействия [помимо поллинга, который работает на сервере точно так же, как и на клиенте, и имеет те же достоинства и недостатки] — отдельный канал связи для обратных вызовов. Этот канал может быть организован в виде очереди сообщений (в частности, через pub/sub топик) либо использование URL обратного вызова (т.н. «webhook»).
|
||||
В отличие от клиентских приложений, серверные API практически безальтернативно используют единственный подход для организации двустороннего взаимодействия [помимо поллинга, который работает на сервере точно так же, как и на клиенте, и имеет те же достоинства и недостатки] — отдельный канал связи для обратных вызовов. В случае публичных API практически безальтернативно такой технологией является использование URL обратного вызова (т.н. «webhook»).
|
||||
|
||||
#### Webhook-и
|
||||
Хотя long polling, WebSocket, MQTT и HTTP/2 Push тоже вполне применимы для backend-2-backend взаимодействия, мы сходу затрудняемся назвать примеры популярных API, которые использовали бы эти технологии. Главными причинами такого положения дел нам видятся:
|
||||
* меньшая критичность к проблемам производительности (у сервера фактически нет ограничений по расходу трафика, и поддержание открытых соединений тоже не является проблемой);
|
||||
* широкий выбор готовых компонентов для разработки webhook-ов (поскольку, фактически, это просто обычный веб-сервер);
|
||||
* возможность описать такое взаимодействие спецификацией и использовать кодогенерацию.
|
||||
|
||||
При интеграции через webhook, партнёр указывает URL своего собственного сервера обработки сообщений, и сервер API вызывает этот эндпойнт для оповещения о произошедшем событии.
|
||||
|
||||
@ -82,26 +85,28 @@ GET /v1/orders/created-history⮠
|
||||
|
||||
Важно, что в любом случае должен существовать формальный контракт (очень желательно — в виде спецификации) на форматы запросов и ответов эндпойнта-webhook-а и возникающие ошибки.
|
||||
|
||||
##### Договорённость о методах авторизации
|
||||
##### Договорённость о авторизации и аутентификации
|
||||
|
||||
Так как webhook-и представляют собой отдельный канал взаимодействия, для него придётся разработать отдельный способ авторизации. Мы повторяем здесь настоятельную рекомендацию не изобретать безопасность и использовать существующие стандартные механизмы, например, [mTLS](https://en.wikipedia.org/wiki/Mutual_authentication#mTLS).
|
||||
Так как webhook-и представляют собой обратный канал взаимодействия, для него придётся разработать отдельный способ авторизации — это партнёр должен проверить, что запрос исходит от нашего бэкенда, а не наоборот. Мы повторяем здесь настоятельную рекомендацию не изобретать безопасность и использовать существующие стандартные механизмы, например, [mTLS](https://en.wikipedia.org/wiki/Mutual_authentication#mTLS), хотя в реальном мире с большой долей вероятности придётся использовать архаичные техники типа фиксации IP-адреса вызывающего сервера.
|
||||
|
||||
##### API для задания адреса webhook-а
|
||||
|
||||
Так как callback-эндпойнт контролируется партнёром, жёстко зафиксировать его обычно не представляется возможным — должен существовать интерфейс (возможно, в виде кабинета партнёра) для задания URL webhook-а (и публичных ключей авторизации).
|
||||
Так как callback-эндпойнт разрабатывается партнёром, его URL нам априори неизвестен. Должен существовать интерфейс (возможно, в виде кабинета партнёра) для задания URL webhook-а (и публичных ключей авторизации).
|
||||
|
||||
**Важно**. К операции задания адреса callback-а нужно подходить с максимально возможной серьёзностью (очень желательно требовать второй фактор авторизации для подтверждения этой операции), поскольку, получив доступ к такой функциональности, злоумышленник может совершить множество весьма неприятных атак:
|
||||
* если указать в качестве приёмника сторонний URL, можно получить доступ к потоку всех заказов партнёра и при этом вызвать перебои в его работе;
|
||||
* такая уязвимость может также эксплуатироваться с целью организации DoS-атаки на сторонние сервисы;
|
||||
* если указать в качестве webhook-а URL интранет-сервисов компании-провайдера API, можно осуществить [SSRF-атаку](https://en.wikipedia.org/wiki/SSRF) на инфраструктуру самой компании.
|
||||
|
||||
#### Типичные проблемы интеграции через webhook
|
||||
|
||||
Двунаправленные интеграции (и клиентские, и серверные — хотя последние в большей степени) несут в себе очень неприятные риски для провайдера API. Если в общем случае качество работы API зависит в первую очередь от самого разработчика API, то в случае обратных вызовов всё в точности наоборот: качество работы интеграции напрямую зависит от того, как код webhook-эндпойнта написан партнёром. Мы можем столкнуться здесь с самыми различными видами проблем в партнёрском коде:
|
||||
* webhook может возвращаеть false-positive ответы, когда сообщение не было обработано, но сервер партнёра тем не менее ошибочно вернул код успеха;
|
||||
* и наоборот, возможны false-negative ответы, когда сообщение было обработано, но эндпойнт почему-то вернул ошибку;
|
||||
* webhook может возвращать false-positive ответы, когда сообщение не было обработано, но сервер партнёра тем не менее ошибочно вернул код успеха;
|
||||
* и наоборот, возможны false-negative ответы, когда сообщение было обработано, но эндпойнт почему-то вернул ошибку (или просто ответил в неправильном формате);
|
||||
* webhook может обрабатывать входящие запросы очень долго — возможно, настолько долго, что сервер сообщений просто не будет успевать их отправить;
|
||||
* могут быть допущены ошибки в реализации идемпотентости, и повторная обработка одного и того же сообщения партнёром может приводить к ошибкам или некорректности данных в системе партнёра;
|
||||
* размер тела сообщение может превысить лимит, выставленный на веб-сервере партнёра;
|
||||
* авторизация на стороне партнёра может не проверяться или проверяться некорректно, и злоумышленник легко может отправить какие-то выгодные ему запросы, представившись сервером API;
|
||||
* наконец, эндпойнт может быть просто недоступен по множеству различных причин, от проблем в дата-центре, где расположены сервера партнёра, до банальной человеческой ошибки при смене URL webhook-а.
|
||||
|
||||
Очевидно, вы никак не можете гарантировать, что партнёр не совершил какую-то из перечисленных ошибок. Но вы можете попытаться минимизировать возможный ущерб:
|
||||
@ -109,8 +114,9 @@ GET /v1/orders/created-history⮠
|
||||
1. Состояние системы должно быть восстановимо. Даже если партнёр неправильно обработал сообщения, всегда должна быть возможность реабилитироваться и получить список последних событий и/или полное состояние системы, чтобы исправить случившиеся ошибки.
|
||||
2. Помогите партнёру написать правильный код, зафиксировав в документации неочевидные моменты, с которыми могут быть незнакомы неопытные разработчики:
|
||||
* ключи идемпотентности каждой операции;
|
||||
* гарантии доставки (exactly once, at least once);
|
||||
* будет ли сервер генерировать параллельные запросы к webhook-у;
|
||||
* гарантии доставки (exactly once, at least once; [см. описание гарантий доставки](https://docs.confluent.io/kafka/design/delivery-semantics.html) на примере технологии Apache Kafka);
|
||||
* будет ли сервер генерировать параллельные запросы к webhook-у и, если да, каково максимальное количество одновременных запросов;
|
||||
* гарантирует ли сервер строгий порядок сообщений (запросы всегда доставляются в порядке от самого старого к самому новому)
|
||||
* размеры полей и сообщений в байтах;
|
||||
* политика перезапросов при получении ошибки.
|
||||
3. Должна быть реализована система мониторинга состояния партнёрских эндпойнтов:
|
||||
@ -119,11 +125,17 @@ GET /v1/orders/created-history⮠
|
||||
|
||||
#### Очереди сообщений
|
||||
|
||||
Хотя long polling, WebSocket, MQTT и HTTP/2 Push тоже вполне применимы для backend-2-backend взаимодействия, мы сходу затрудняемся назвать примеры популярных API, которые использовали бы эти технологии. Главными причинами такого положения дел нам видятся:
|
||||
* меньшая критичность к проблемам производительности (у сервера фактически нет ограничений по расходу трафика, и поддержание открытых соединений тоже не является проблемой);
|
||||
* широкий выбор готовых компонентов для разработки webhook-ов (поскольку, фактически, это просто обычный веб-сервер);
|
||||
* возможность описать такое взаимодействие спецификацией и использовать кодогенерацию.
|
||||
Для внутренних API технология webhook-ов (то есть наличия программной возможности задавать URL обратного вызова) либо вовсе не нужна, либо решается с помощью протоколов [Service Discovery](https://en.wikipedia.org/wiki/Web_Services_Discovery), поскольку сервисы в составе одного бэкенда как правило равноправны — если сервис А может вызывать сервис Б, то и сервис Б может вызывать сервис А.
|
||||
|
||||
Однако при разработке взаимодействия бэкенд-бэкенд гораздо большее значение имеют гарантии доставки и отсутствие ошибок в обвязке, поскольку в случае проявления каких-то проблем во взаимодействии,
|
||||
Однако все проблемы Webhook-ов, описанные нами выше, для таких обратных вызовов всё ещё актуальны. Вызов внутреннего сервиса всё ещё может окончиться false negative-ошибкой, внутренние клиенты могут не ожидать нарушения порядка пересылки сообщений и так далее.
|
||||
|
||||
Для внутренних API оба способа являются равноценными, а вот в партнёрских и внешних API webhook-и — фактически единственный способ организации взаимодействия.
|
||||
Для решения этих проблем, а также для большей горизонтальной масштабируемости технологий обратного вызова, были созданы [сервисы очередей сообщений](https://en.wikipedia.org/wiki/Message_queue) и, в частности, различные серверные реализации паттерна pub/sub. В настоящий момент pub/sub-архитектуры пользуются большой популярностью среди разработчиков, вплоть до перевода любого межсервисного взаимодействия на очереди событий.
|
||||
|
||||
**NB**: отметим, что ничего бесплатного в мире не бывает, и за эти гарантии доставки и горизонтальную масштабируемость необходимо платить:
|
||||
* межсерверное взаимодействие становится событийно-консистентным со всеми вытекающими отсюда проблемами;
|
||||
* хорошая горизонтальная масштабируемость и дешевизна использования очередей достигается при использовании политик at least once/at most once и отсутствии гарантии строгого порядка событий;
|
||||
* в очереди могут скапливаться необработанные сообщения, внося нарастающие задержки, и решение этой проблемы на стороне подписчика может оказаться отнюдь не тривиальным.
|
||||
|
||||
Отметим также, что в публичных API зачастую используются обе технологии в связке — бэкенд API отправляет задание на вызов webhook-а в виде публикации события, которое специально предназначенный для этого внутренний сервис будет пытаться обработать путём вызова webhook-а.
|
||||
|
||||
Теоретически можно представить себе и такую интеграцию, в которой разработчик API даёт партнёрам непосредственно прямой доступ к внутренней очереди сообщений, однако примеры таких API нам неизвестны.
|
Loading…
x
Reference in New Issue
Block a user