Работаем с HTTP API: разбор частых ошибок и методы их решения

8c185ae091fdb44ac2a5a10dd65fa92a.jpg

Время идёт, технологии развиваются, а проблемы, связанные с использованием API, вызывают у многих разработчиков всё те же сложности, что и десятки лет назад. Между тем, рост числа сервисов, которые взаимодействуют друг с другом с помощью данного способа, день ото дня только увеличивается, и неумение надёжно, качественно и безопасно работать с API может привести к опасным сбоям или поломке разрабатываемой вами системы.

Привет, Хабр! Я веб-разработчик, который за годы своей деятельности не раз налаживал взаимодействие реализуемых мной систем с другими через различные виды HTTP API. Бизнес-модели некоторых создаваемых мной приложений и вовсе строились только лишь на одном таком взаимодействии: к примеру, однажды мне нужно было сделать сервис для отложенного размещения постов в различные социальные сети, и умение обрабатывать ошибки, связанные с использованием их API, было критично важным.

При работе над подобными проектами предвидеть все проблемы заранее попросту невозможно. И это касается не только веб-разработки: подобные сложности (равно как и способы их решения) схожи в системах различного типа. А поскольку с развитием технологий взаимодействие между различными сервисами через API день ото дня всё более востребовано, то и умение правильно выстроить работу с АПИ становится для каждого разработчика ключевым. Для подтверждения этого факта, не нужно ходить далеко за примерами: так, с большими языковыми моделями, наподобие OpenAI GPT-3.5 или GPT-4, ежедневно взаимодействуют тысячи сервисов, и это взаимодействие происходит именно по API. Об огромном интересе интеграции со стороны разработчиков можно судить, например, по количеству звёзд на гитхабе у OpenAI Сookbook (более 53000).

Ещё немного статистики: большинство современных API являются HTTP API (такими как REST и GraphQL), и по результатам исследования Postman »2023 State of the API Report», REST (и его подвиды) остается самой популярной архитектурой — её используют 86% респондентов. По этой причине в статье я больше сосредоточусь на проблемах, связанных скорее с REST, хотя многие упомянутые мной решения могут быть актуальны и для других подходов. Моя цель — предоставить информацию, которая поможет разработчикам избегать самых распространенных ошибок, улучшая тем самым качество их систем.

Статья получилась объёмной, поэтому для более удобной навигации сразу добавлю оглавление:

»418 I’m a teapot» — что же может пойти не так?

Итак, вы только приступили к разработке своего веб, десктоп, мобильного или ещё какого-нибудь приложения, которое должно получать информацию из внешнего источника с помощью API. На первый взгляд, задача может показаться несложной: скорее всего, вы уже располагаете хорошей или не очень документацией по взаимодействию со сторонним сервисом и примерами его интеграции. Всего только и нужно, что просто написать код, который будет отправлять запросы из вашего приложения, верно? Увы, нет…. Ведь на деле всё гораздо сложнее, чем в теории. Вас может ожидать множество трудностей, начиная от проблем с производительностью внешней системы и заканчивая вопросами безопасности. Каждая из них по-своему подрывает стабильность вашей разработки: иногда риски маленькие, и ими можно пренебречь в угоду скорости создания проекта. Но зачастую проблемы нарастают как снежный ком. Именно поэтому не стоит недооценивать их сложность, чтобы всегда быть готовым к различным препятствиям на пути к успешной интеграции с внешним API.

Проблемы решаются легче, если заранее подготовить стратегию по их проработке: следуя перечисленным ниже правилам, можно минимизировать влияние сторонних сервисов на стабильность создаваемого вами проекта.

»500 Internal Server Error» — решаем проблемы доступности API

Начнём разбор с одной из самых частых ошибок, которую иногда допускают даже самые опытные разработчики систем, взаимодействующих с API сторонних сервисов. Это некорректная обработка случаев, когда запрашиваемый ресурс поломан или вовсе недоступен. 

Даже у крупных сервисов, таких как GitHub, могут происходить сбои, которые влияют на работу зависимых от них приложений. А, к примеру, статистика по инцидентам и uptime для Discord показывает, что доступность их API далеко не всегда составляет 100%: раз в несколько месяцев происходят какие-либо неполадки. Это означает, что сервисы, использующие API Discord в своих приложениях, подвержены рискам, и могут тоже ломаться, если заранее не предусмотреть и не реализовать систему обработки ошибок. 
Но если даже такие известные сервисы иногда ломаются, что же тогда говорить о менее популярных?  

Давайте разоберёмся с тем, как можно решать проблемы, связанные с доступностью ресурсов. Конечно, подходы могут несколько разниться в зависимости от того, для какой цели вы осуществляете API-запросы.

Работаем с ошибками обновления данных по API

Для обработки ошибок при обновлении данных на стороннем сервисе список вариантов решения будет следующим:

  • Асинхронное взаимодействие. Очереди запросов. В очень многих случаях, хорошей практикой будет использование очередей для отправки запросов на внешние ресурсы. Такой подход позволит избежать задержек при взаимодействии пользователей с вашим сервисом при недоступности внешнего API: вашей системе попросту не придётся ждать, чтобы отдать ответ пользователю, прежде чем отправка данных в сторонний сервис успешно завершится. Кроме того, подобный метод позволит вам получить больше контроля над тем, как вы работаете со сторонним ресурсом. Например, используя его, вы сможете контролировать количество отправляемых запросов в момент времени.

  • Не забывайте повторять попытки отправки данных в случае неудачи. В случае ошибок при отправке запроса реализуйте логику для автоматических повторных попыток через некоторые временные интервалы. Интервалы лучше не делать фиксированными, и ниже я расскажу почему. А информацию обо всех осуществлённых запросах следует сохранять для контроля работы вашей системы и анализа стабильности работы стороннего сервиса. Данный функционал будет легче реализовать после того, как вы прислушаетесь к первому пункту и создадите очереди задач. Но, конечно, можно обойтись и без них — всё зависит от того, как сделано ваше приложение.
    Важно: не делайте повторные запросы при ошибках с кодами 4xxбез дополнительных действий по исправлению некорректного запроса. Дело в том, что коды ошибок, которые начинаются на 4, обычно говорят о том, что сам запрос был отправлен некорректно (это клиентские ошибки).

  • Будьте внимательны к управлению очередью задач. Ранее я рассказал, почему очередь задач на отправку API запросов может помочь вам. Но используя её, важно помнить, что слишком большое количество одновременно выполняемых задач может попросту перегрузить и вашу систему, и используемый вами сторонний сервис. Именно поэтому необходимо сбалансировать частоту и количество повторных попыток отправки данных. Для этого можно использовать небольшую рандомизацию времени повтора неудачного запроса. Или даже лучше — методику экспоненциального откладывания каждого следующего вызова API (exponential backoff). Так вы снизите вероятность одномоментной отправки большого количества повторных запросов и не положите на лопатки вашу очередь и сервис, с которым взаимодействуете.

  • Избегайте повторной отправки одинаковых данных. Если сторонний сервис недоступен какое-то время, это может привести к тому, что в очереди на повторную отправку появятся дублирующие или лишние API запросы. К примеру, пользователь отредактировал свой профиль из состояния A в состояние B, затем из состояния B в состояние C, и после этого снова вернул все в состояние A, а при каждом редактировании профиля вы должны были обновить данные на стороннем сервисе через API. В этом примере ваша система может пытаться выполнить лишние запросы, ведь в итоге данные возвращаются в изначальное состояние, и значит отправку всей этой цепочки вероятно следует остановить. Если бы мы остановились в состоянии C, отправка всей цепочки запросов тоже не имела бы смысла. В этом случае можно объединить все данные изменений в ожидании на отправку и получить diff относительно изначального состояния (конечно же, учитывая при этом последовательность правок). Работая только с diff-ом, конечный запрос вы получите лишь один, а в дополнение сможете избежать и проблем, связанных с гонкой состояний.

  • Информирование пользователя. После нескольких неудачных попыток отправки API запроса, не забудьте уведомить пользователя о проблеме. Он скорее всего и не в курсе, что ваша система зависит от стороннего сервиса, а значит может попросту не дождаться, когда необходимый ему функционал начнёт работать. Данный совет особенно актуален, если без отправки данных в другой сервис ваша система не может качественно продолжать работу. Помните, что при работе с внешними API может случиться всякое. Например, используемый вами в работе сервис может попросту закрыть доступ к API или внезапно сделать его платным (что как раз недавно произошло с Twitter/X). Именно поэтому важно предусмотреть подобные сценарии и постараться снизить негативные эмоции ваших пользователей, дав понять им, что над сложившейся проблемой уже идёт работа и скоро она будет завершена.

Обрабатываем ошибки при запросе данных по API

Для кейсов, где вы запрашиваете данные через API, способы и подходы решения немного другие, но есть и общие пункты:

  • И снова очереди. Представим ситуацию: в реализованном вами продукте регистрируется новый пользователь, но для завершения этого процесса вам нужно подгрузить данные из стороннего сервиса. Если этот сервис и его API сломается, то таймаут операции на вашем сайте будет слишком долгим, регистрация не пройдёт, и вы, возможно, потеряете клиента. Поэтому мы действуем здесь, как и в случае с отправкой данных в сторонний сервис через АПИ — подгружаем данные через очередь каждый раз, когда это возможно, снижая таким образом время отклика вашей системы.

  • Повторные попытки запроса и управление очередью задач. Тут всё то же, как и в пункте по обновлению данных. Если вы реализовали подгрузку необходимых данных из сторонней системы через очередь, доработать систему для управления этой очередью не составит большого труда.

  • Запрос данных напрямую от пользователя, если сторонний сервис недоступен. Такой способ решения возможен во многих случаях: вы можете честно сказать, что данные не загрузились, и попросить пользователя ввести их самостоятельно. Конечно, этот вариант не закроет все кейсы, ведь бывают случаи, когда нужно подгрузить историю заказов или какую-то статистику, которую пользователь, само собой, ввести не в состоянии. Но если это, к примеру, просто список интересов пользователя, спросить об этом будет легче, чем продолжать пытаться вытащить эти данные из неработающей сторонней системы. 

  • Кэширование. Не следует каждый раз пытаться получить одно и то же со стороннего сервиса: используйте кэш, который заодно поможет в тех случаях, когда запрашиваемый ресурс будет недоступен. Ну, а если же данные рискуют быть изменёнными, кэш можно использовать как запасной аэродром — просто предупредите вашего пользователя, что они могли устареть.

  • Дополнительный источник получения данных. Никакой API не может называться безопасным и стабильным источником данных, а значит по возможности следует предусмотреть и запасной вариант. К примеру, вы занимаетесь разработкой сервиса, который анализирует историю покупок и продаж акций на бирже. В этом случае для вас будет доступно сразу несколько различных провайдеров API, предоставляющих исторические данные по акциям. А следовательно, одного из них можно использовать как fallback-вариант. Да, такой способ будет доступен вам далеко не во всех сценариях, но, если вам повезло, и дополнительный источник имеется — обязательно берите его на вооружение.

Дополнительные советы

Кроме перечисленного, для всех случаев работы с API важно производить настройку connection timeout и request timeout запросов. Это поможет избежать чрезмерной нагрузки на вашу систему, предотвращая ситуацию, когда запросы выполняются слишком долго.

Определяем connection timeout и request timeout помощью curl

Выбор значения таймаута — задача непростая, и зависит от большого количества факторов. Connection timeout обычно выставляется меньше чем request timeout, потому что процесс установления соединения, как правило, занимает меньше времени, чем обработка этого запроса сервером. Хотя, конечно, все зависит от того, где расположены сервера API-провайдера. Чтобы подобрать подходящие значения, разумным решением будет сначала собрать статистику по работе данного API в shadow mode. Такие данные легко получить через curl с использованием опции --write-out:

curl -o /dev/null -s -w "Time to connect: %{time_connect}\nTime to start transfer: %{time_starttransfer}\n" https://google.com

В libcurl эту информацию можно получить через метод curl_easy_getinfo, где необходимые данные будут возвращены в CURLINFO_CONNECT_TIME и CURLINFO_STARTTRANSFER_TIME. 

После сбора необходимых метрик, используя статистические данные, вы сможете определить порог допустимых таймаутов для работы с данным API.

Ещё одним полезным действием будет добавление систем мониторинга и оповещений. Если сторонний сервис перестал отвечать, вы должны узнавать об этом мгновенно. А если API начинает выдавать слишком большое количество ошибок, необходимо предусмотреть систему, которая автоматически снизит поток отправляемых запросов в момент времени.

»429 Too Many Requests» — работаем с лимитированием количества запросов

Теперь, когда мы решили проблемы, связанные с недоступностью АПИ, следует подумать о том, как бороться с лимитами на количество запросов. Этот вопрос тоже весьма актуален и может всплыть в самый неподходящий момент. Более того, провайдер API может эти лимиты совершенно неожиданно изменить, поэтому жизненно важно предусмотреть подобную ситуацию заранее, чтобы решить все сложности максимально быстро.

Часть прописанных выше советов подойдёт и здесь. Например, кэширование ответов может спасти вас от необходимости слишком частой отправки API-запросов, поскольку вероятность превышения лимитов тогда будет заметно ниже.

Дополнительная информация о кэше в HTTP

Отмечу, что в HTTP API есть масса удобных механизмов по работе со временем жизни кэша by design.

Например, для получения времени жизни кэша можно использовать заголовок Expires.

Cache-Control может использоваться для задания инструкций кеширования.

ETag — является идентификатором версии ресурса. Если ресурс будет изменён, ETag также изменится.

Ну, а Last-Modified показывает, когда запрашиваемый ресурс был изменён в последний раз. Но при наличии ETag, лучше ориентироваться именно на него, так как он считается более надёжным: Last-Modified имеет ограничение в виде единицы времени (обычно в секундах), что может не отражать мелкие изменения. Кроме этого, ETag будет более точным в случае, если ресурс был изменен, но его содержимое осталось прежним.

При этом все перечисленные выше заголовки можно получить при запросе типа HEAD. Он, как правило, не ограничивается показателем рейт лимита или ограничивается значительно большим лимитом, чем другие типы запросов.

Как и в случае с обработкой ситуаций при недоступности API, кроме реализации кэширования, эффективно использовать очередь запросов и механизм для повторной отправки запроса (возможно с реализацией diff-логики). Этот подход можно назвать «золотым стандартом» для сценариев, когда не всегда возможно получить ответ от стороннего сервиса мгновенно. Управление очередями запросов помогает обеспечить непрерывную работу приложения, даже если сторонний API временно сбоит. Но есть и дополнительные рекомендации для предотвращения проблем с rate-limiting:

  • Работайте в каждом отдельном запросе с большим количеством полезных (но не лишних) данных. Например, во многих реализациях REST API существует способ получить сразу целый набор элементов по определённым фильтрам. Но не забывайте, по возможности, запрашивать только необходимые поля, чтобы не гонять лишние данные по сети. POST / PATCH запросы для создания или обновления записей в некоторых API тоже поддерживают операции сразу с набором сущностей. Конечно REST является лишь набором рекомендаций, в жизни доступные возможности зависят от реализации API, и подобный функционал может отсутствовать. Однако, практика показала, что всегда можно связаться с разработчиками и попросить их внедрить нужные функции. Попробуйте: у меня не раз получалось. Хуже точно не будет!

  • Старайтесь распределить запросы по времени. Когда мы обсуждали проблемы доступности API, я уже предлагал добавлять случайное время для повторной отправки запроса. Но иногда, для обхода лимитов требуется реализовать более сложный механизм. Для его реализации лучше заранее изучить документацию и выяснить все существующие ограничения.

  • Используйте ключи API от ваших пользователей, если это возможно. В некоторых сценариях, при наличии явного согласия пользователей, можно использовать их API-ключи для отправки запросов на сторонние сервисы. Это может быть полезным для обхода ограничений на количество запросов, установленных этими сервисами. Одним из распространенных методов является использование технологии OAuth. OAuth позволяет пользователям предоставлять ограниченный доступ к своим данным через токены, исключая необходимость передачи логина и пароля. Важно отметить, что при использовании такого подхода необходимо строго соблюдать принципы безопасности. Для этого следует обеспечить надлежащее информирование пользователей о способах использования их данных, а также гарантировать безопасное хранение и обработку API-ключей и OAuth токенов. Кроме того, необходимо убедиться, что такое использование API-ключей соответствует политике конфиденциальности сторонних сервисов и законодательству о защите данных.

  • Применяйте Callback-API там, где это возможно. Многие сервисы предоставляют и такой тип API в дополнение к REST, так как он является идеальным вариантом для того, чтобы предотвратить бессмысленную отправку лишних запросов. С помощью данного метода вы просто подписываетесь на определённые события, не опрашивая сторонний сервис регулярно. Опять же, реализация данного функционала может сильно варьироваться в зависимости от того, кто этот API предоставляет. Тем не менее, есть и стандарты. Например, в спецификации OpenAPI 3 определено, как правильно работать с коллбэками. Но используя данный метод вы всегда должны помнить, что предоставляя URL вашего ресурса для callback-вызова, следует скрывать IP-адрес реального сервера. Кроме того, используемый домен не должен быть очевиден для злоумышленников: обо всём этом я писал в своей предыдущей статье о правилах backend-разработки.

Кстати, используя вышеперечисленные советы, вы не только уменьшаете вероятность ошибок, но и экономите бюджет, если доступ к API — платный. Так что, это именно тот случай, когда можно убить сразу двух зайцев и сэкономить патроны.

»451 Unavailable For Legal Reasons» — ответ от API не всегда такой, каким вы его ожидаете

Сторонний АПИ может вернуть буквально всё что угодно. Поправочка:, а может и не вернуть то, что вы хотели от него получить. Ведь не у всех есть хорошие методы для версионирования, а иногда их и нет вовсе. Так что порой, даже один и тот же запрос может сегодня возвращать одно, а завтра — совершенно другое. Мораль: не забывать о таком варианте развития событий и никогда не полагаться полностью на сторонних разработчиков.

К слову, проблема не всегда связана с изменением версий. Бывают случаи, когда после блокировки ресурса, интернет-провайдеры вместо какого-нибудь JSON-а начинают возвращать HTML с информацией о блокировке. Или, например, сервисы защиты от DDoS могут подменять контент, также возвращая HTML с капчей для проверки пользователя. Да, второй кейс могут предусмотреть создатели API, но на практике такое происходит далеко не всегда.  Вот, что поможет в этой ситуации:

  • Валидация возвращаемых данных. Это очень важный шаг. Сразу выполняя валидацию ответа, вы уменьшаете вероятность ошибки при дальнейшей работе с полученными данными в коде. К возвращаемым данным, которые стоит проверять, относится не только response body, но и вообще любая информация, которую вы используете в приложении, например, какие-нибудь заголовки, так как они тоже могут быть подвержены неожиданным изменениям.

  • Использование API-proxy для приведения данных к ожидаемому формату. Минимизировать риск ошибок в приложении при взаимодействии с API (особенно в случаях его незначительных изменений) иногда помогает прокси. Специально настроенный вами прокси-сервер может помочь приводить полученные данные к необходимому формату, что особенно полезно, когда API часто обновляется и модифицируется. Прокси-сервер может сглаживать несоответствия между версиями API и структурой ожидаемых данных в приложении. Существует ряд решений, которые могут подойти для этой цели, и даже предоставить огромное количество дополнительных возможностей. Например, вам может подойти Apigee — мощный платный сервис от Google. Но помните, что любой API-proxy также подвержен ошибкам и проблемам, поэтому всегда следует быть начеку.

  • И тут тоже нужна система мониторинга и логирования всех проблем, связанных с неправильными ответами от используемого API. А настроив оповещения, вы сможете реагировать на любые ошибки максимально быстро.

  • Снижение зависимости от данных, получаемых со стороннего сервиса. В очередной раз напомню эту прописную истину, чтобы она прочно засела в памяти: взаимодействие со сторонними продуктами всегда несёт в себе немалые риски, поэтому, чем оно меньше, тем лучше.

  • Отслеживание обновлений и новостей провайдера API. Может случиться так, что используемый вами API будет изменён, или же какие-нибудь его функции будут помечены как устаревшие для удаления в будущем. Мониторинг новостей и изменений поможет подготовиться к некоторым потенциальным сложностям, ведь многие правки могут анонсироваться заранее.

  • Своевременное обновление версий API. Если используемый вами API обладает различными версиями, не тяните слишком долго с обновлениями, ведь рано или поздно старые версии прекращают поддерживать, а по прошествии большого количества времени обновление может стать слишком дорогим и болезненным.

  • Не игнорируйте перенаправления. Может произойти и такая ситуация, когда вы успешно внедрили API, но через какое-то время его разработчики решили добавить редирект. К примеру, на другой домен. Или же, у API-эндпоинтов раньше не было https, а когда его добавили, решили сразу всех перевести на безопасное соединение. Для того, чтобы ваша интеграция не сломалась от этой правки, лучше всегда следовать редиректам. В случае использования libcurl вам в этом поможет опция CURLOPT_FOLLOWLOCATION.

»426 Upgrade Required» — не забываем о безопасности

Вот о чём, а о безопасности пользователи API думают не так часто. А зря, ведь злоумышленникам это очень хорошо известно. Бывалые взломщики могут воспользоваться вашей невнимательностью и хакнуть разрабатываемую вами систему, и для этого у них приготовлена масса способов. Кстати, и данную тему я частично раскрыл в своей предыдущей статье, но напомнить о таких серьёзных проблемах и способах их решения никогда не будем лишним.

Разработчик, реализующий взаимодействие со сторонним API, всегда должен помнить следующие правила:

  • Ни в коем случае не храните API-ключи или секреты в коде вашего приложения. Даже если вы думаете, что к вашему коду уж точно никто не получит доступ, или что разрабатываемая вами система вообще мало кому интересна, к сожалению, у хакеров своё мнение на этот счёт. И особенно им нравятся рабочие ключи для доступа к АПИ сторонних сервисов, которые, кстати, могут быть и платными.

  • Скрывайте реальный IP вашего сервера при отправке запроса. Зная IP, нехорошие люди могут сделать очень много нехороших вещей с вашим проектом и сервером. Например, элементарно его заDDOSить. Для безопасности обязательно используйте набор прокси-серверов, через которые должны идти все запросы с вашего сервера. Таким образом, в крайнем случае плохо будет не вашему реальному серверу, а лишь бедному прокси, который, если что, легко можно заменить на новый.

  • Не доверяйте возвращаемым данным от стороннего API. Дело в том, что кроме абсолютно безопасной информации, через ответы от АПИ злоумышленники могут попытаться произвести SQL-инъекцию или XSS атаку. И не зря, ведь большое количество разработчиков даже не задумывается о том, что так вообще может быть. Все возвращаемые данные от сторонних сервисов обязательно необходимо фильтровать, прежде чем выводить на своём ресурсе или сохранять куда-нибудь. Выше я советовал логировать ответы от стороннего сервиса: так вот, знайте, что данный тип атаки можно провести и через HTTP заголовки. Например, если вы решили вывести в админку какой-нибудь заголовок, который вернул сервер при ответе на запрос, без фильтрации контента — это верный способ нарваться на XSS атаку. Конечно, тут я привёл крайне редкий пример, но его реализация возможна. А это значит, что нужно быть готовым и к такому развитию событий.

  • Не отправляйте через API секретную информацию без острой необходимости. Старайтесь следить за тем, какую информацию вы отправляете в запросе. В любой его части: в заголовках, параметрах, теле. Будьте бдительны всегда, а с персональными данными ваших клиентов — тем более, ведь утечка такой информации может нести за собой серьезные юридические риски. Например можно попасть на штраф в 60 000 рублей.

  • Старайтесь всегда использовать HTTPS для API запросов. Выбирая между http и https всегда используйте второй вариант: так вы существенно снизите риск утечки информации.

»200 OK» — теперь вы знаете, как работать с основными рисками API

В этой статье я попытался осветить наиболее часто встречающиеся проблемы API и методы их решения. Главное, что должен помнить каждый: при работе со сторонними API не стоит верить в их абсолютную надежность. Проблемы могут возникнуть там, где вы их меньше всего ожидаете, поэтому крайне важно тщательно продумывать архитектуру и стратегии обработки ошибок в ваших приложениях заранее, чтобы максимально уменьшить потенциальные риски и обеспечить устойчивость работы вашего сервиса в будущем.

© Habrahabr.ru