Працюємо з HTTP API: розбір частих проблем та методи їх вирішення
Час іде, технології розвиваються, а проблеми, пов’язані з використанням API, викликають у багатьох розробників ті самі складності, що й десятки років тому. Тим часом, зростання числа сервісів, які взаємодіють один з одним за допомогою даного способу, з кожним днем тільки збільшується, і невміння надійно, якісно і безпечно працювати з 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-адресу реального сервера. Крім того, домен, що використовується, не повинен бути очевидним для зловмисників.
До речі, використовуючи перелічені вище поради, ви не тільки зменшуєте ймовірність помилок, але й економите бюджет, якщо доступ до API – платний. Отже, це саме той випадок, коли можна вбити одразу двох зайців та заощадити патрони.
«451 Unavailable For Legal Reasons» – відповідь від API не завжди така, якою ви її очікуєте
Сторонній АНІ може повернути буквально все, що завгодно. Виправлення: а може і не повернути те, що ви хотіли від нього отримати. Адже не всі мають хороші методи для версіонування, а іноді їх і немає зовсім. Тож часом навіть один і той же запит може сьогодні повертати одне, а завтра – зовсім інше. Мораль: не забувати про такий варіант розвитку подій і ніколи не покладатися повністю на сторонніх розробників.
До речі, проблема не завжди пов’язана із зміною версій. Бувають випадки, коли після блокування ресурсу інтернет-провайдери замість якогось JSON-а починають повертати HTML з інформацією про блокування. Або, наприклад, сервіси захисту від DDoS можуть замінювати контент, також повертаючи HTML з капчею для перевірки користувача. Так, другий кейс можуть передбачити творці API, але на практиці таке відбувається далеко не завжди. Ось що допоможе в цій ситуації:
- Валідація даних, що повертаються. Це дуже важливий крок. Відразу, виконуючи валідацію відповіді, ви зменшуєте ймовірність помилки при подальшій роботі з отриманими даними в коді. До даних, що повертаються, які варто перевіряти, відноситься не тільки response body, але й взагалі будь-яка інформація, яку ви використовуєте в додатку, наприклад, які-небудь заголовки, так як вони теж можуть бути схильні до несподіваних змін.
- Використання API-прокси для приведення даних до очікуваного формату. Мінімізувати ризик помилок у додатку при взаємодії з 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 секретну інформацію без необхідності. Намагайтеся стежити за тим, яку інформацію ви надсилаєте у запиті. У будь-якій його частині: у заголовках, параметрах, тілі. Будьте пильні завжди, а з персональними даними ваших клієнтів – тим більше, адже витік такої інформації може нести серйозні юридичні ризики.
- Намагайтеся завжди використовувати HTTPS для API запитів . Вибираючи між http і https завжди використовуйте другий варіант: так ви суттєво зменшите ризик витоку інформації.
“200 OK” – тепер ви знаєте, як працювати з основними ризиками API
У цій статті я спробував висвітлити проблеми API, що найчастіше зустрічаються, і методи їх вирішення. Головне, що повинен пам’ятати кожен: під час роботи зі сторонніми API не варто вірити в їхню абсолютну надійність. Проблеми можуть виникнути там, де ви на них найменше очікуєте, тому вкрай важливо ретельно продумувати архітектуру та стратегії обробки помилок у ваших додатках заздалегідь, щоб максимально зменшити потенційні ризики та забезпечити стійкість роботи вашого сервісу в майбутньому.