При какой HTTP ошибке нельзя повторять запрос
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# HTTP ошибки и повторные попытки (Retries)
Главное правило: различай идемпотентные и неидемпотентные операции
Повторять запрос или нет зависит от типа ошибки и типа HTTP метода. Есть ошибки, при которых повторять НЕЛЬЗЯ, потому что это может привести к двойной обработке.
Ошибки, при которых НЕЛЬЗЯ повторять запрос
1. 4xx — Ошибки клиента (обычно НЕ повторяют)
Это означает, что клиент что-то сделал неправильно. Повтор не поможет:
import requests
from requests.exceptions import HTTPError
def safe_request_with_retry(url, data):
"""Правильная обработка ошибок с повторами"""
for attempt in range(3):
try:
response = requests.post(url, json=data)
response.raise_for_status() # Выбросит HTTPError
return response.json()
except HTTPError as e:
status = e.response.status_code
# 4xx ошибки — НЕ повторяем
if 400 <= status < 500:
print(f"Клиентская ошибка {status}: не будет повторено")
raise # Сразу выбрасываем
# 5xx ошибки — ПОВТОРЯЕМ
elif 500 <= status < 600:
print(f"Серверная ошибка {status}: повторяем попытку {attempt + 1}")
if attempt == 2:
raise # Последняя попытка
else:
raise
2. 400 Bad Request — невалидные данные
Клиент отправил невалидные данные. Повтор того же запроса только выбросит ту же ошибку.
# ❌ ПЛОХО: повторяем запрос с ошибкой
def bad_retry():
for _ in range(3):
response = requests.post('https://api.example.com/data',
json={"age": "not_a_number"}) # Невалидно
if response.status_code == 400:
continue # Повторяем бесполезно!
# ✅ ХОРОШО: исправляем данные перед повтором
def good_retry():
data = {"age": "invalid"}
response = requests.post('https://api.example.com/data', json=data)
if response.status_code == 400:
# Исправляем ошибку в данных
data["age"] = 30 # Правильный формат
response = requests.post('https://api.example.com/data', json=data)
return response
3. 401 Unauthorized — проблема с аутентификацией
Токен истёк или некорректен. Просто повторить запрос не поможет.
def request_with_auth():
headers = {"Authorization": "Bearer invalid_token"}
for attempt in range(3):
response = requests.get('https://api.example.com/user', headers=headers)
if response.status_code == 401:
# НЕ просто повторяем, а обновляем токен
headers["Authorization"] = f"Bearer {get_new_token()}"
# Теперь повторяем с новым токеном
response = requests.get('https://api.example.com/user', headers=headers)
break
return response
def get_new_token():
# Получить новый токен от сервера
return "new_valid_token"
4. 403 Forbidden — нет прав доступа
Пользователь не имеет прав. Повторное выполнение не изменит права.
def request_with_permission():
response = requests.get('https://api.example.com/admin/data')
if response.status_code == 403:
# НЕ повторяем просто так
# Нужно предоставить правильные права
print("Доступ запрещён. Требуется повышение прав")
raise PermissionError("Insufficient permissions")
return response
5. 404 Not Found — ресурс не существует
Ресурс не найден. Повтор не поможет (разве что ожидаем, что ресурс будет создан).
def get_user(user_id):
"""Попытка получить пользователя"""
url = f'https://api.example.com/users/{user_id}'
for attempt in range(3):
response = requests.get(url)
if response.status_code == 404:
# Не повторяем — ресурс не существует
print(f"Пользователь {user_id} не найден")
return None
elif response.status_code == 200:
return response.json()
return None
6. 409 Conflict — конфликт состояния
Операция конфликтует с текущим состоянием. Зависит от контекста.
def update_resource():
# Попытка обновить ресурс с версией
data = {"name": "New Name", "version": 1}
response = requests.put('https://api.example.com/resource/123', json=data)
if response.status_code == 409:
# Конфликт версий — нужно получить актуальную версию
current = requests.get('https://api.example.com/resource/123').json()
data["version"] = current["version"]
# Повторяем с обновленной версией
response = requests.put('https://api.example.com/resource/123', json=data)
return response
7. 422 Unprocessable Entity — невозможно обработать
Данные синтаксически правильны, но логически неприемлемы. Повтор не поможет.
def create_user():
# Email уже существует — это бизнес-логика, не ошибка сервера
data = {"name": "John", "email": "existing@example.com"}
response = requests.post('https://api.example.com/users', json=data)
if response.status_code == 422:
# НЕ повторяем это же значение
errors = response.json()["errors"]
print(f"Ошибка валидации: {errors}")
# Пользователь должен изменить email
return None
return response.json()
Ошибки, при которых МОЖНО/НУЖНО повторять запрос
Серверные ошибки (5xx)
import time
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
def requests_with_retry():
"""Правильная стратегия повторных попыток"""
session = requests.Session()
# Стратегия повторов
retry_strategy = Retry(
total=3, # Максимум 3 попытки
backoff_factor=1, # 1, 2, 4 секунды между попытками
status_forcelist=[429, 500, 502, 503, 504], # Повторяем при этих кодах
allowed_methods=["HEAD", "GET", "OPTIONS", "PUT", "DELETE"] # Безопасные методы
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)
return session
session = requests_with_retry()
response = session.get('https://api.example.com/data')
429 Too Many Requests — лимит запросов
def request_with_rate_limit():
"""Обработка лимита частоты запросов"""
for attempt in range(5):
response = requests.get('https://api.example.com/data')
if response.status_code == 429:
# Получаем Retry-After заголовок
retry_after = int(response.headers.get('Retry-After', 60))
print(f"Лимит достигнут, ждём {retry_after} сек")
time.sleep(retry_after)
# ПОВТОРЯЕМ запрос
continue
elif response.status_code == 200:
return response.json()
raise Exception("Не удалось получить данные после 5 попыток")
503 Service Unavailable — сервис недоступен
def request_with_service_unavailable():
"""Обработка временной недоступности"""
max_retries = 3
for attempt in range(max_retries):
response = requests.get('https://api.example.com/data')
if response.status_code == 503:
# Сервис временно недоступен, ПОВТОРЯЕМ
if attempt < max_retries - 1:
wait_time = 2 ** attempt # Экспоненциальная задержка
print(f"Сервис недоступен, повтор через {wait_time} сек")
time.sleep(wait_time)
continue
else:
raise Exception("Сервис недоступен после 3 попыток")
elif response.status_code == 200:
return response.json()
return None
Таблица: какие ошибки повторять
| Код | Описание | Повторять | Причина |
|---|---|---|---|
| 400 | Bad Request | ❌ НЕТ | Невалидные данные клиента |
| 401 | Unauthorized | ⚠️ УСЛОВНО | Нужно обновить авторизацию |
| 403 | Forbidden | ❌ НЕТ | Нет прав доступа |
| 404 | Not Found | ❌ НЕТ | Ресурс не существует |
| 409 | Conflict | ⚠️ УСЛОВНО | Зависит от типа конфликта |
| 422 | Unprocessable | ❌ НЕТ | Бизнес-логика не позволяет |
| 429 | Too Many Requests | ✅ ДА | Временное ограничение |
| 500 | Internal Server Error | ✅ ДА | Временная проблема сервера |
| 502 | Bad Gateway | ✅ ДА | Сетевая проблема |
| 503 | Service Unavailable | ✅ ДА | Сервер временно недоступен |
| 504 | Gateway Timeout | ✅ ДА | Timeout в сетевой цепи |
Важный момент: идемпотентность методов
# GET, HEAD, OPTIONS, DELETE — идемпотентные (безопасны для повтора)
def safe_request():
# Можно повторять при любых ошибках
response = requests.get('https://api.example.com/data') # ✅
response = requests.delete('https://api.example.com/data/123') # ✅
# POST, PUT (без идемпотентного ключа) — НЕ идемпотентные
def unsafe_request():
# НЕЛЬЗЯ просто повторять при ошибке
response = requests.post('https://api.example.com/users',
json={"name": "John"}) # ❌ может создать дубликат
# Если сервер не ответил, мы не знаем, был ли создан пользователь
# Повтор может создать дубликат!
Правильная стратегия повторов
import requests
import time
from requests.exceptions import RequestException
class SmartRetry:
@staticmethod
def should_retry(status_code, method):
"""Определяет, стоит ли повторять запрос"""
# Никогда не повторяем 4xx (кроме 429)
if 400 <= status_code < 500 and status_code != 429:
return False
# Всегда повторяем 5xx
if 500 <= status_code < 600:
return True
# Повторяем 429
if status_code == 429:
return True
return False
@staticmethod
def request_with_retry(method, url, max_retries=3, **kwargs):
"""Выполняет запрос с правильной стратегией повторов"""
for attempt in range(max_retries):
try:
response = requests.request(method, url, **kwargs)
if SmartRetry.should_retry(response.status_code, method):
if attempt < max_retries - 1:
wait_time = 2 ** attempt
print(f"Повтор через {wait_time} сек (попытка {attempt + 1})")
time.sleep(wait_time)
continue
else:
raise Exception(f"Не удалось выполнить запрос после {max_retries} попыток")
return response
except RequestException as e:
if attempt < max_retries - 1:
time.sleep(2 ** attempt)
continue
else:
raise
return None
# Использование
response = SmartRetry.request_with_retry('GET', 'https://api.example.com/data')
print(response.json())
Заключение
НЕ повторяйте запрос при:
- 400-499 (кроме 429) — ошибки клиента, повтор не поможет
- 401, 403 — проблемы с авторизацией
- 404 — ресурс не существует
- 422 — невозможно обработать (бизнес-логика)
ПОВТОРЯЙТЕ запрос при:
- 429 — лимит частоты запросов (используйте Retry-After)
- 500-599 — серверные ошибки (используйте exponential backoff)
- Таймауты и сетевые ошибки
Всегда учитывайте идемпотентность метода: POST обычно опасен для повтора, GET всегда безопасен.