Python · API · Просунутий рівень

Обробка помилок

5 завдань

Надійно обробляйте HTTP-помилки, мережеві збої та таймаути в API-скриптах.

Обробка помилок у requests

#
Реальні скрипти для роботи з API зазнають збоїв. Сервер може повернути 404, мережа може відключитися, запит може перевищити час очікування. Явна обробка цих випадків робить скрипти надійними. Є два типи збоїв: 1. **HTTP-помилки** -- сервер відповідає, але з кодом помилки (4xx або 5xx). За замовчуванням `requests` не кидає виняток -- він повертає відповідь, і виклик `.json()` може повернути payload помилки замість даних. 2. **Мережеві помилки** -- запит взагалі не доходить до сервера або сервер не відповідає. Ці помилки кидають винятки: ConnectionError, Timeout або базовий RequestException. `raise_for_status()` усуває прогалину для HTTP-помилок -- кидає HTTPError при статусі 4xx або 5xx: ```python import requests response = requests.get('https://apilearn.tukas.dev/api/products/99999/') response.raise_for_status() # кидає HTTPError при 404 data = response.json() # виконується тільки якщо статус 2xx ``` Для мережевих помилок використовуйте try/except: ```python try: response = requests.get(url, timeout=5) response.raise_for_status() data = response.json() except requests.exceptions.Timeout: print('Запит перевищив час очікування') except requests.exceptions.ConnectionError: print('Мережева помилка -- не вдалося зв'язатися з сервером') except requests.exceptions.HTTPError as e: print(f'HTTP помилка: {e.response.status_code}') ``` Параметр `timeout=` встановлює ліміт часу в секундах. Без нього зависаючий сервер може зупинити скрипт назавжди: ```python # Кидає Timeout якщо немає відповіді протягом 5 секунд response = requests.get(url, timeout=5) # Окремі таймаути для підключення і читання response = requests.get(url, timeout=(3, 10)) # (connect, read) ``` Ієрархія винятків requests (від конкретних до загальних): ``` RequestException Базовий клас для всіх винятків requests ConnectionError Не вдалося зв'язатися з сервером ProxyError Помилка підключення через проксі Timeout Запит перевищив час очікування ConnectTimeout Таймаут підключення ReadTimeout Таймаут читання HTTPError Статус 4xx або 5xx (raise_for_status) TooManyRedirects Цикл перенаправлень ``` Перехоплення `RequestException` обробляє всі типи одразу -- корисно для простих скриптів, де різниця між типами помилок не важлива.

Практичні паттерни обробки помилок

#
Практичні прийоми обробки помилок API: Базова обгортка, яка перетворює помилки на зрозумілі повідомлення: ```python import requests def safe_get(url, **kwargs): try: response = requests.get(url, timeout=5, **kwargs) response.raise_for_status() return response.json() except requests.exceptions.Timeout: print(f'Помилка: запит до {url} перевищив час очікування') except requests.exceptions.ConnectionError: print(f'Помилка: не вдалося підключитися до {url}') except requests.exceptions.HTTPError as e: print(f'HTTP помилка: {e.response.status_code}') return None ``` Окреме перехоплення 401 для повідомлення про проблему з авторизацією: ```python try: response = requests.get(url, headers=headers) response.raise_for_status() except requests.exceptions.HTTPError as e: if e.response.status_code == 401: print('Помилка авторизації — перевірте токен') else: print(f'HTTP помилка: {e.response.status_code}') ``` Retry з exponential backoff для тимчасових збоїв (429, 503): ```python import time def retry_get(url, retries=3, **kwargs): for attempt in range(retries): try: response = requests.get(url, timeout=5, **kwargs) response.raise_for_status() return response.json() except requests.exceptions.HTTPError as e: if e.response.status_code in (429, 503) and attempt < retries - 1: wait = 2 ** attempt # 1s, 2s, 4s print(f'Спроба {attempt + 1} невдала, чекаємо {wait}s...') time.sleep(wait) else: raise return None ``` Exponential backoff означає: чекати 1 секунду після першої невдачі, 2 секунди після другої, 4 секунди після третьої — так ми не заважаємо серверу, який перевантажений.

Довідник обробки помилок

#
Довідник по обробці помилок: **raise_for_status()** ```python response.raise_for_status() # Кидає HTTPError при статусі 4xx або 5xx # Нічого не робить при 2xx або 3xx # e.response.status_code — код статусу у блоці except ``` **timeout=** ```python requests.get(url, timeout=5) # 5 секунд для підключення + відповіді requests.get(url, timeout=(3, 10)) # (підключення, читання) окремо # Кидає requests.exceptions.Timeout ``` **Ієрархія винятків** ``` RequestException — перехоплює будь-яку помилку requests ├── ConnectionError — мережева помилка, DNS ├── Timeout — перевищено час очікування └── HTTPError — 4xx/5xx від raise_for_status() ``` **Канонічний шаблон try/except** ```python try: response = requests.get(url, timeout=5) response.raise_for_status() data = response.json() except requests.exceptions.Timeout: print('Перевищено час очікування') except requests.exceptions.ConnectionError: print('Немає з'єднання') except requests.exceptions.HTTPError as e: print(f'HTTP {e.response.status_code}') ``` Порядок `except` важливий: специфічніші класи ставте вище — `Timeout` і `ConnectionError` перед `RequestException`.
01

Використання raise_for_status()

#

Зробіть GET до /api/products/99999/ (неіснуючий продукт). Без raise_for_status() виведіть код статусу. Потім додайте raise_for_status() і обробіть виняток HTTPError, вивівши повідомлення про помилку.

import requests

BASE_URL = 'https://apilearn.tukas.dev'

# Частина 1: без raise_for_status — перевірте код статусу
response = requests.get(f'{BASE_URL}/api/products/99999/')
print(response.status_code)

# Частина 2: з raise_for_status і обробкою HTTPError
Рішення
import requests

BASE_URL = 'https://apilearn.tukas.dev'

# Частина 1: без raise_for_status
response = requests.get(f'{BASE_URL}/api/products/99999/')
print(response.status_code)  # 404

# Частина 2: з raise_for_status
try:
    response = requests.get(f'{BASE_URL}/api/products/99999/')
    response.raise_for_status()
except requests.exceptions.HTTPError as e:
    print(f'HTTP помилка: {e.response.status_code}')
02

Обробка помилки авторизації

#

Спробуйте отримати /api/users/profile/ БЕЗ заголовка авторизації. Обробіть HTTPError і виведіть спеціальне повідомлення, якщо статус 401.

import requests

BASE_URL = 'https://apilearn.tukas.dev'

# GET профілю без авторизації — обробіть 401 окремо
try:
    response = requests.get(f'{BASE_URL}/api/users/profile/')
    response.raise_for_status()
except requests.exceptions.HTTPError as e:
    # Виведіть різні повідомлення для 401 і інших кодів
    pass
Рішення
import requests

BASE_URL = 'https://apilearn.tukas.dev'

try:
    response = requests.get(f'{BASE_URL}/api/users/profile/')
    response.raise_for_status()
except requests.exceptions.HTTPError as e:
    if e.response.status_code == 401:
        print('Помилка авторизації — надайте дійсний токен')
    else:
        print(f'HTTP помилка: {e.response.status_code}')
03

Додати timeout до запиту

#

Зробіть GET до /api/products/ із timeout=5. Обробіть Timeout і ConnectionError окремо. Виведіть список продуктів при успіху або повідомлення про помилку.

import requests

BASE_URL = 'https://apilearn.tukas.dev'

# GET з timeout і обробкою мережевих помилок
Рішення
import requests

BASE_URL = 'https://apilearn.tukas.dev'

try:
    response = requests.get(f'{BASE_URL}/api/products/', timeout=5)
    response.raise_for_status()
    products = response.json()['results']
    print(f'Отримано {len(products)} продуктів')
except requests.exceptions.Timeout:
    print('Помилка: запит перевищив час очікування')
except requests.exceptions.ConnectionError:
    print('Помилка: не вдалося підключитися до API')
except requests.exceptions.HTTPError as e:
    print(f'HTTP помилка: {e.response.status_code}')
04

Функція safe_get()

#

Напишіть функцію safe_get(url, **kwargs), яка робить GET з timeout=5, викликає raise_for_status() і повертає JSON при успіху або None при будь-якій помилці (виводячи повідомлення). Протестуйте з валідним і неіснуючим URL.

import requests

BASE_URL = 'https://apilearn.tukas.dev'

def safe_get(url, **kwargs):
    # Реалізуйте з timeout, raise_for_status і обробкою всіх помилок
    pass

# Тест 1: валідний URL
result = safe_get(f'{BASE_URL}/api/products/')
if result:
    print(f'Продуктів: {len(result["results"])}')

# Тест 2: неіснуючий ендпоінт
result = safe_get(f'{BASE_URL}/api/products/99999/')
print(result)
Рішення
import requests

BASE_URL = 'https://apilearn.tukas.dev'

def safe_get(url, **kwargs):
    try:
        response = requests.get(url, timeout=5, **kwargs)
        response.raise_for_status()
        return response.json()
    except requests.exceptions.Timeout:
        print(f'Помилка: timeout при запиті до {url}')
    except requests.exceptions.ConnectionError:
        print(f'Помилка: немає з'єднання з {url}')
    except requests.exceptions.HTTPError as e:
        print(f'HTTP {e.response.status_code} для {url}')
    return None

result = safe_get(f'{BASE_URL}/api/products/')
if result:
    print(f'Продуктів: {len(result["results"])}')

result = safe_get(f'{BASE_URL}/api/products/99999/')
print(result)
05

Retry з backoff

#

Напишіть функцію retry_get(url, retries=3), яка повторює запит при кодах 429 або 503, очікуючи 2**attempt секунд між спробами. При інших помилках — одразу кидати виняток. Виведіть повідомлення перед кожним очікуванням.

import requests
import time

BASE_URL = 'https://apilearn.tukas.dev'

def retry_get(url, retries=3, **kwargs):
    # Повторюйте при 429/503, exponential backoff, кидайте при інших помилках
    pass

# Тест із робочим ендпоінтом (не активує retry)
result = retry_get(f'{BASE_URL}/api/products/')
print(result['count'])
Рішення
import requests
import time

BASE_URL = 'https://apilearn.tukas.dev'

def retry_get(url, retries=3, **kwargs):
    for attempt in range(retries):
        try:
            response = requests.get(url, timeout=5, **kwargs)
            response.raise_for_status()
            return response.json()
        except requests.exceptions.HTTPError as e:
            if e.response.status_code in (429, 503) and attempt < retries - 1:
                wait = 2 ** attempt
                print(f'Спроба {attempt + 1} невдала ({e.response.status_code}), чекаємо {wait}s...')
                time.sleep(wait)
            else:
                raise
    return None

result = retry_get(f'{BASE_URL}/api/products/')
print(result['count'])