1
0
mirror of https://github.com/twirl/The-API-Book.git synced 2025-06-24 22:36:43 +02:00

REST continued

This commit is contained in:
Sergey Konstantinov
2023-02-04 14:12:29 +02:00
parent e15befa98d
commit b58721d84d
2 changed files with 80 additions and 8 deletions

View File

@ -30,10 +30,17 @@
Главное преимущество, которое вам предоставляет следование букве стандарта HTTP — возможность положиться на то, что промежуточные агенты, от клиентских фреймворков до API-гейтвеев, умеют читать метаданные запроса и выполнять какие-то действия с их использованием — настраивать политику перезапросов и таймауты, логировать, кэшировать, шардировать, проксировать и так далее — без необходимости писать какой-то дополнительный код. Главное преимущество, которое вам предоставляет следование букве стандарта HTTP — возможность положиться на то, что промежуточные агенты, от клиентских фреймворков до API-гейтвеев, умеют читать метаданные запроса и выполнять какие-то действия с их использованием — настраивать политику перезапросов и таймауты, логировать, кэшировать, шардировать, проксировать и так далее — без необходимости писать какой-то дополнительный код.
Главным недостатком HTTP API является то, что промежуточные агенты, от клиентских фреймворков до API-гейтвеев, умеют читать метаданные запроса и выполнять какие-то действия с их использованием — настраивать политику перезапросов и таймауты, логировать, кэшировать, шардировать, проксировать и так далее — даже если вы их об этом не просили. Более того, так как стандарты HTTP являются сложными, концепция REST — непонятной, а разработчики программного обеспечения — неидеальными, то промежуточные агенты могут трактовать метаданные запроса *неправильно*. Особенно это касается каких-то экзотических и сложных в имплементации стандартов.
Если попытаться сформулировать главный принцип разработки HTTP API, то мы получим примерно следующее: **лучше бы ты разрабатывал 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 являются сложными, концепция REST — непонятной, а разработчики программного обеспечения — неидеальными, то промежуточные агенты могут трактовать метаданные запроса *неправильно*. Особенно это касается каких-то экзотических и сложных в имплементации стандартов.
#### Вопросы производительности #### Вопросы производительности
В пользу многих современных альтернатив HTTP API — таких как GraphQL, gRPC, Apache Thrift — часто приводят аргумент о низкой производительности JSON-over-HTTP API по сравнению с рассматриваемой технологией; конкретнее, называются следующие проблемы: В пользу многих современных альтернатив HTTP API — таких как GraphQL, gRPC, Apache Thrift — часто приводят аргумент о низкой производительности JSON-over-HTTP API по сравнению с рассматриваемой технологией; конкретнее, называются следующие проблемы:
@ -52,18 +59,18 @@
2. Вообще говоря, если такая нужда появляется, то и в рамках HTTP API вполне можно регулировать список возвращаемых полей ответа, это вполне соответствует духу и букве стандарта. Однако, мы должны заметить, что экономия трафика на возврате частичных состояний (которую мы рассматривали подробно в главе «Частичные обновления») очень редко бывает оправдана. 2. Вообще говоря, если такая нужда появляется, то и в рамках HTTP API вполне можно регулировать список возвращаемых полей ответа, это вполне соответствует духу и букве стандарта. Однако, мы должны заметить, что экономия трафика на возврате частичных состояний (которую мы рассматривали подробно в главе «Частичные обновления») очень редко бывает оправдана.
3. Если использовать стандартные (де)сериализаторы JSON, разница по сравнению с бинарными форматами может оказаться действительно очень большой. Если, однако, эти накладные расходы являются проблемой, стоит обратиться к альтернативным сериализаторам — в частности, [simdjson](https://github.com/simdjson/simdjson). Благодаря оптимизированному низкоуровневому коду simdjson показывает отличную производительность, которой уж точно хватит для обычного HTTP API. 3. Если использовать стандартные десериализаторы JSON, разница по сравнению с бинарными форматами может оказаться действительно очень большой. Если, однако, эти накладные расходы являются проблемой, стоит обратиться к альтернативным десериализаторам — в частности, [simdjson](https://github.com/simdjson/simdjson). Благодаря оптимизированному низкоуровневому коду simdjson показывает отличную производительность, которой уж точно хватит для обычного HTTP API.
4. Вообще говоря, парадигма HTTP API подразумевает, что для бинарных данных (такие как изображения или видеофайлы) предоставляются отдельные эндпойнты. Передача бинарных данных в теле JSON-ответа необходима только в случаях, когда отдельный запрос за ними представляет собой проблему с точки зрения производительности. Такой проблемы фактически не существует в server-2-server взаимодействии, и даже в мобильных сетях она уже далеко не так остра. 4. Вообще говоря, парадигма HTTP API подразумевает, что для бинарных данных (такие как изображения или видеофайлы) предоставляются отдельные эндпойнты. Передача бинарных данных в теле JSON-ответа необходима только в случаях, когда отдельный запрос за ними представляет собой проблему с точки зрения производительности. Такой проблемы фактически не существует в server-2-server взаимодействии, и даже в мобильных сетях она уже далеко не так остра.
5. Протокол HTTP 1.1 действительно неидеален с точки зрения мультиплексирования запросов. Однако альтернативные парадигмы организации API для решения этой проблемы опираются… на HTTP версии 2.0. Разумеется, и HTTP API можно построить поверх версии 2.0. 5. Протокол HTTP 1.1 действительно неидеален с точки зрения мультиплексирования запросов. Однако альтернативные парадигмы организации API для решения этой проблемы опираются… на HTTP версии 2.0. Разумеется, и HTTP API можно построить поверх версии 2.0.
На всякий случай ещё раз уточним: JSON-over-HTTP API *действительно* проигрывает с точки зрения производительности современным бинарным протоколам, таким как gRPC. Мы, однако, берём на себя смелость утверждать, что производительность хорошо спроектированного и оптимизированного HTTP API достаточна для абсолютного большинства предметных областей, и реальный выигрыш от перехода на альтернативные протоколы окажется незначителен. На всякий случай ещё раз уточним: JSON-over-HTTP API *действительно* проигрывает с точки зрения производительности современным бинарным протоколам. Мы, однако, берём на себя смелость утверждать, что производительность хорошо спроектированного и оптимизированного HTTP API достаточна для абсолютного большинства предметных областей, и реальный выигрыш от перехода на альтернативные протоколы окажется незначителен.
#### Преимущества и недостатки формата JSON #### Преимущества и недостатки формата JSON
Как нетрудно заметить, большинство претензий, предъявляемых к концепции HTTP API, относятся вовсе не к HTTP, а к использованию формата JSON. В самом деле, ничто не мешает разработать API, которое будет использовать любой бинарный формат вместо JSON (включая тот же Protobuf), и тогда разница между Protobuf-over-HTTP API и gRPC сведётся только к (не)возможности использовать то или иное стандартное программное обеспечение «из коробки». Как нетрудно заметить, большинство претензий, предъявляемых к концепции HTTP API, относятся вовсе не к HTTP, а к использованию формата JSON. В самом деле, ничто не мешает разработать API, которое будет использовать любой бинарный формат вместо JSON (включая тот же Protobuf), и тогда разница между Protobuf-over-HTTP API и gRPC сведётся только к (не)использованию подробных URL, статус-кодов и заголовков запросов и ответов (и вытекающей отсюда (не)возможности использовать то или иное стандартное программное обеспечение «из коробки»).
Однако, во многих случаях (включая настоящую книгу) разработчики предпочитают текстовый JSON бинарным Protobuf (Flatbuffers, Thrift, AVRO и т.д.) по очень простой причине: JSON очень легко и удобно читать. Во-первых, он текстовый и не требует дополнительной расшифровки; во-вторых, имена полей включены в сам файл. Если Protobuf невозможно прочитать без .proto-файла, то по JSON-документу почти всегда можно попытаться понять, что за данные в нём описаны. Однако, во многих случаях (включая настоящую книгу) разработчики предпочитают текстовый JSON бинарным Protobuf (Flatbuffers, Thrift, Avro и т.д.) по очень простой причине: JSON очень легко и удобно читать. Во-первых, он текстовый и не требует дополнительной расшифровки; во-вторых, имена полей включены в сам файл. Если сообщение в формате protobuf невозможно прочитать без .proto-файла, то по JSON-документу почти всегда можно попытаться понять, что за данные в нём описаны. В совокупности с тем, что при разработке HTTP API мы также стараемся следовать стандартной семантике всей остальной обвязки, в итоге мы получаем API, запросы и ответы к которому (по крайней мере в теории) удобно читаются и интуитивно понятны.
Помимо человекочитаемости у JSON есть ещё одно важное преимущество: он максимально формален. В нём нет никаких конструкций, которые могут быть по-разном истолкованы в разных архитектурах (с точностью до ограничений на длины чисел и строк), и при этом он удобно ложится в нативные структуры данных почти любого языка программирования. С этой точки зрения у нас фактически не было никакого другого выбора, какой ещё формат данных мы могли бы использовать при написании этой книги. В случае же разработки API менее общего назначения, мы рекомендуем подходить к выбору формата по тому же принципу: взвесить накладные расходы на подготовку и внедрение инструментов чтения бинарных форматов и расшифровки бинарных протоколов против накладных расходов на неоптимальную (де)сериализацию и использование разнородного ПО различного качества. Помимо человекочитаемости у JSON есть ещё одно важное преимущество: он максимально формален. В нём нет никаких конструкций, которые могут быть по-разном истолкованы в разных архитектурах (с точностью до ограничений на длины чисел и строк), и при этом он удобно ложится в нативные структуры данных почти любого языка программирования. С этой точки зрения у нас фактически не было никакого другого выбора, какой ещё формат данных мы могли бы использовать при написании примеров кода для этой книги. В случае же разработки API менее общего назначения, мы рекомендуем подходить к выбору формата по тому же принципу: взвесить накладные расходы на подготовку и внедрение инструментов чтения бинарных форматов и расшифровки бинарных протоколов против накладных расходов на неоптимальную передачу данных и использование разнородного ПО при разработке API.

View File

@ -1 +1,66 @@
### Семантика и основные понятия ### Семантика протокола HTTP
Итак, после трёх вступительных глав с прояснением основных терминов и понятий (такова, увы, цена популярности технологии) мы перейдём, наконец, непосредственно к вопросам дизайна HTTP API.
В описании семантики и формата протокола мы будем руководствоваться свежевышедшим [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html), который заменил аж девять предыдущих RFC, описывавших разные аспекты технологии (правда, большое количество различной дополнительной функциональсти всё ещё покрывается отдельными стандартами, например, метод `PATCH` так и не вошёл в основной стандарт и всё ещё регулируется [RFC 5789](https://www.rfc-editor.org/rfc/rfc5789)).
HTTP-запрос представляет собой применение определённого глагола к URL с указанием версии протокола и передачей дополнительной мета-информации в заголовках и, возможно, каких-то данных в теле запроса:
```
POST /v1/orders HTTP/2.0
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-запрос будет являться конструкция, состоящая из статус-кода ответа, сообщения, заголовков и, возможно, тела ответа.
```
201 Created
Location: /v1/orders/123
Content-Type: application/json
{
"id": 123
}
```
### URL
URL — единица адресации в HTTP API (некоторые евангелисты технологии даже используют термин «пространство URL» как синоним для Мировой паутины). Предполагается, что API в парадигме REST должно использовать систему адресов столь же гранулярную, как и предметная область; иными словами, у любых сущностей, которыми мы можем манипулировать независимо, должен быть свой URL. При этом
Формат URL регулируется [отдельным стандартом](https://url.spec.whatwg.org/), который развивает независимое сообщество Web Hypertext Application Technology Working Group (WHATWG).
URL принято раскладывать на составляющие, каждая из которых имеет свою семантику (и может отсутствовать):
* схема — протокол обращения (в нашем случае всегда `https:`);
* хост (домен или IP-адрес), к которому обращён запрос — самая крупная единица адресации;
* домен может включать в себя поддомены;
* порт;
* путь — часть URL между доменным именем (с портом) и символами `?`, `#` или концом строки
* путь традиционно считается способом выразить строгую иерархию сущностей; если сущность `leaf` полностью вложена в сущность `root`, этот факт отражают в виде path `/root/leaf`;
* путь принято разбивать по символу `/` и работать с каждой частью как отдельным токеном — но стандарт этого вообще-то не предписывает;
* пути с символом `/` и без символа `/` в конце (скажем `/root/leaf` и `/root/leaf/`) вообще говоря являются разными (и URL, отличающиеся только наличием/отсутствием слэша, считаются разными URL), хотя практически нам неизвестны аргументы в пользу того, чтобы не считать такие пути эквивалентными;
* пути могут содержать секции `.` и/или `..`, которые предлагается трактовать аналогичным символам в путях на файловой системе (и, соответственно, считать URL `/root/leaf`, `/root/./leaf`, `/root/branch/../leaf` эквивалентными), но стандарт этого вновь не предписывает;
* query — часть URL после знака `?` до знака `#` или конца строки;
* query принято раскладывать на пары ключ=значение, разделённые символом `&`; следует вновь иметь в виду, что стандарт не предписывает query строго соответствовать этому формату;
* фрагмент (якорь) — часть URL после знака `#`;
* фрагмент традиционно рассматривается как адресация внутри запрошенного документа, поэтому многими агентами опускается при выполнении запроса;
* два URL с различными значениями фрагмента могут считаться одинаковыми — а могут не считаться, зависит от контекста.
В HTTP-запросах, как правило (но не обязательно) схема, хост и порт опускаются (и считаются совпадающими с параметрами соединения).
**NB**: в стандарте также перечислены разнообразные исторические наслоения (например, передача логинов и паролей в URL или использование не-UTF кодировки), которые нам в рамках вопросов дизайна API неинтересны. Также стандарт содержит правила сериализации, нормализации и сравнения URL, которые могут оказаться полезны.
### Заголовки
Заголовки — это *метаинформация*, привязанная к запросу или ответу. Она может описывать какие-то свойства передаваемых данных (например, `Content-Length`), дополнительные сведения о клиенте или сервере (`User-Agent`, `Date`), или просто содержать какие-то поля, не относящиеся непосредственно к смыслу запроса/ответа (например, `Authorization`).
Важное свойство заголовков — это возможность считывать их до того, как получено тело сообщения. Таким образом, заголовки могут, во-первых, сами по себе влиять на обработку запроса или ответа, и ими можно относительно легко манипулировать при проксировании.