07 — Async HTTP Clients: правильні зовнішні запити¶
Навіщо це потрібно¶
Ти вже вмієш писати async views і знаєш про sync_to_async. Тепер — найпоширеніший реальний use case: зовнішні HTTP-запити.
Кожен Django backend рано чи пізно звертається до зовнішніх API: платіжні шлюзи, погодні сервіси, мікросервіси. І тут починається проблема — якщо ти використовуєш requests, ти блокуєш event loop.
🧠 Ментальна модель¶
Уяви кур'єра, що розвозить листи.
Sync-кур'єр (requests): їде до першої адреси, стоїть біля дверей і чекає, поки господар відкриє. Тільки після того — до другої адреси. Третя — тільки після другої.
Async-кур'єр (httpx): залишає листи у всіх поштових скриньках одночасно. Не чекає, поки відкриють двері. Просто кидає в скриньку і летить до наступного.
Один async-кур'єр встигає більше, ніж три sync-кур'єри.
Ключові терміни¶
| Термін | Що означає |
|---|---|
requests |
Стандартна sync HTTP-бібліотека Python |
httpx |
Modern HTTP-клієнт: підтримує sync і async режими |
aiohttp |
Async-only HTTP-клієнт (повністю асинхронний) |
httpx.AsyncClient |
Async-варіант httpx-клієнта |
asyncio.gather |
Запуск кількох async-операцій паралельно |
| Connection pool | Кеш відкритих з'єднань для повторного використання |
Чому requests блокує async view¶
requests — чудова бібліотека для sync-коду. Але всередині async view вона небезпечна:
import requests
from django.http import JsonResponse
async def weather_view(request):
# ❌ НЕПРАВИЛЬНО: requests.get блокує весь event loop!
response = requests.get("https://api.example.com/weather")
return JsonResponse(response.json())
Що відбувається:
1. requests.get() — синхронна функція
2. Вона блокує OS-потік до отримання відповіді
3. Але OS-потік — це наш event loop!
4. Всі інші async-запити "підвисають" на весь час очікування
Якщо зовнішній API відповідає за 3 секунди — весь твій сервер заморожений на 3 секунди для всіх користувачів.
Таблиця: requests vs httpx vs aiohttp¶
| Характеристика | requests |
httpx |
aiohttp |
|---|---|---|---|
| Режим | Тільки sync | Sync і async | Тільки async |
| API схожість | — | Майже ідентичний requests |
Свій API |
| ASGI сумісність | ❌ Блокує event loop | ✅ Повна | ✅ Повна |
| HTTP/2 | ❌ | ✅ | Частково |
| Складність переходу | — | Мінімальна | Потребує навчання |
| Рекомендація | Legacy WSGI apps | Modern ASGI Django | High-performance async |
Рекомендація для Django ASGI: використовуй httpx.AsyncClient. API майже ідентичний requests — мінімальна крива навчання.
Один запит: sync vs async¶
❌ Синхронний запит (блокує)¶
import requests
from django.http import JsonResponse
def sync_weather_view(request):
# Sync view + sync client — правильна пара для WSGI
response = requests.get("https://api.example.com/weather?city=Kyiv")
return JsonResponse(response.json())
Під WSGI — це нормально. Під ASGI в async view — небезпечно.
✅ Асинхронний запит (не блокує)¶
import httpx
from django.http import JsonResponse
async def async_weather_view(request):
city = request.GET.get("city", "Kyiv")
async with httpx.AsyncClient() as client:
# await: view призупиняється, event loop обслуговує інших
response = await client.get(
f"https://api.example.com/weather?city={city}",
timeout=10.0
)
data = response.json()
return JsonResponse({
"city": city,
"temperature": data.get("temp"),
"description": data.get("desc")
})
async with httpx.AsyncClient() as client — правильний спосіб. Клієнт:
- автоматично переробляє з'єднання (connection pooling)
- закривається після async with блоку
- не тримає ресурси відкритими
Кілька паралельних запитів через gather¶
Найбільша перевага async HTTP — паралельні запити:
sequenceDiagram
participant V as Django View
participant API1 as API 1 (weather)
participant API2 as API 2 (news)
participant API3 as API 3 (stocks)
Note over V, API3: Синхронний варіант — 3 секунди
V->>API1: requests.get() (1s)
API1-->>V: відповідь
V->>API2: requests.get() (1s)
API2-->>V: відповідь
V->>API3: requests.get() (1s)
API3-->>V: відповідь
Note over V, API3: Асинхронний варіант — ~1 секунда
V->>API1: await client.get()
V->>API2: await client.get()
V->>API3: await client.get()
Note over V: Event loop вільний!
API1-->>V: відповідь
API2-->>V: відповідь
API3-->>V: відповідь
Код паралельних запитів¶
import asyncio
import httpx
from django.http import JsonResponse
async def dashboard_view(request):
async with httpx.AsyncClient(timeout=10.0) as client:
# Визначаємо всі запити
weather_req = client.get("https://api.example.com/weather?city=Kyiv")
news_req = client.get("https://api.example.com/news?lang=uk")
stocks_req = client.get("https://api.example.com/stocks?ticker=AAPL")
# Запускаємо всі три одночасно
# Час = час найповільнішого з трьох (не сума!)
weather, news, stocks = await asyncio.gather(
weather_req,
news_req,
stocks_req,
return_exceptions=True # Не падаємо якщо один API недоступний
)
return JsonResponse({
"weather": weather.json() if not isinstance(weather, Exception) else None,
"news": news.json() if not isinstance(news, Exception) else None,
"stocks": stocks.json() if not isinstance(stocks, Exception) else None,
})
return_exceptions=True дуже важливий: якщо один із трьох API недоступний — gather повертає exception як значення замість того, щоб зупинити все.
Event Loop Diagram: Sync vs Async HTTP¶
graph TD
A[Браузер запит] --> B[Django Async View]
B --> C{HTTP Client}
C -->|requests.get| D[Потік заблокований]
D --> E[Сервер не обслуговує нікого]
C -->|await httpx.get| F[Yield до Event Loop]
F --> G[Event Loop обслуговує інших]
G --> H[Відповідь від API]
H --> I[View відновлюється]
style D fill:#ffebee,stroke:#c62828
style E fill:#ffebee,stroke:#c62828
style F fill:#e8f5e9,stroke:#2e7d32
style G fill:#e8f5e9,stroke:#2e7d32
Правильне використання AsyncClient¶
✅ Context manager (рекомендовано)¶
async def view(request):
async with httpx.AsyncClient() as client:
response = await client.get("https://api.example.com/data")
# client автоматично закрито
return JsonResponse(response.json())
✅ Повторне використання клієнта (для production)¶
Якщо view викликається часто — краще тримати один клієнт на весь час роботи додатка:
# services/http_client.py
import httpx
# Один клієнт для всього додатка
_client = None
async def get_client() -> httpx.AsyncClient:
global _client
if _client is None:
_client = httpx.AsyncClient(timeout=10.0)
return _client
# views.py
from services.http_client import get_client
async def my_view(request):
client = await get_client()
response = await client.get("https://api.example.com/data")
return JsonResponse(response.json())
Типова помилка початківця¶
❌ requests у async view без обгортки¶
import requests
async def bad_view(request):
# Блокує весь сервер!
data = requests.get("https://api.example.com/data").json()
return JsonResponse(data)
✅ Якщо не можеш використати httpx — обгортай у sync_to_async¶
import requests
from asgiref.sync import sync_to_async
@sync_to_async
def fetch_data_sync():
return requests.get("https://api.example.com/data").json()
async def view(request):
# Тепер sync-виклик у окремому потоці — event loop вільний
data = await fetch_data_sync()
return JsonResponse(data)
Це не так ефективно як httpx, але безпечно.
Практичне завдання¶
Завдання 1¶
Встанови httpx (pip install httpx) і напиши async view, який:
1. Робить GET-запит до https://httpbin.org/get
2. Повертає JsonResponse з полями url і headers з відповіді
Завдання 2¶
Напиши dashboard_view, який паралельно запитує три URL через asyncio.gather():
- https://httpbin.org/delay/1
- https://httpbin.org/delay/1
- https://httpbin.org/delay/1
Заміряй час виконання. Має бути ~1 секунда, а не 3.
Завдання 3¶
Поясни: чому return_exceptions=True важливий у asyncio.gather() для продакшн-коду? Що відбудеться без цього прапорця, якщо один з трьох API недоступний?
Самоперевірка¶
- [ ] Я розумію, чому
requestsнебезпечний в async view - [ ] Я можу написати async view з
httpx.AsyncClient - [ ] Я вмію запускати кілька HTTP-запитів паралельно через
gather - [ ] Я знаю, що
return_exceptions=Trueзахищає від падіння при помилці одного з запитів - [ ] Я знаю, як безпечно обернути
requestsу sync view черезsync_to_async
Підсумок¶
requests — синхронна бібліотека. Всередині async view вона блокує весь event loop. Правильна альтернатива — httpx.AsyncClient з await.
Головна перевага async HTTP: asyncio.gather() дозволяє запускати кілька запитів паралельно. Три API по 1 секунді → ~1 секунда загального часу замість 3.
Для продакшну: використовуй return_exceptions=True у gather, щоб один недоступний API не ламав всю відповідь.