Ошибка 412 (Precondition Failed) при загрузке по URL, который выдал мессенджер (обычно presigned URL), чаще всего возникает потому, что сервер ожидает определённые заголовки / размер тела / хэш содержимого, а вы их не передаёте или передаёте неверно. Частые причины:
- не задан Content-Length (при использовании генератора requests будет ставить chunked transfer, а некоторые серверы не принимают chunked или проверяют длину);
- ожидается точный Content-Type (например video/mp4);
- ожидается контрольная сумма (Content-MD5 или похожий заголовок);
- ожидались специфичные заголовки (If-Match, x-amz-* и т.п.).
Ниже — два рабочих подхода с прогресс-баром tqdm: 1) если URL — presigned PUT (тогда нужно отправлять "raw" тело и указать Content-Length), 2) если это обычный multipart/form-data POST к API (тогда удобнее использовать requests-toolbelt для отслеживания прогресса).
1) Presigned PUT (рекомендуемый способ для "raw" загрузок)
- Обязательно вычислите размер файла и передайте Content-Length — тогда requests не будет использовать chunked.
- Читайте файл порциями и отдавайте их генератором, который обновляет tqdm.
Пример:
```python
import os
import requests
from tqdm import tqdm
def upload_put_with_progress(url: str, filepath: str, chunk_size: int = 1024*1024):
file_size = os.path.getsize(filepath)
headers = {
'Content-Type': 'video/mp4', # или тот Content-Type, который требует сервер
'Content-Length': str(file_size),
}
with open(filepath, 'rb') as f:
with tqdm(total=file_size, unit='B', unit_scale=True, unit_divisor=1024) as pbar:
def gen():
while True:
chunk = f.read(chunk_size)
if not chunk:
break
pbar.update(len(chunk))
yield chunk
# используем PUT, data=генератор, но Content-Length уже указан
resp = requests.put(url, data=gen(), headers=headers)
resp.raise_for_status()
return resp
```
Ключевые моменты:
- Content-Length обязателен для многих presigned URL. Если его не указать — Requests пошлёт chunked transfer encoding и сервер может вернуть 412/411.
- Укажите Content-Type, который ожидает сервер. Иногда presigned URL подписан под конкретный Content-Type — он должен совпадать.
- Если сервер ожидает Content-MD5, см. пункт ниже.
2) Если API требует multipart/form-data (обычный upload endpoint)
- requests сам формирует тело и заголовки, но трудно отслеживать прогресс. Для этого удобно requests-toolbelt:
Установите:
pip install requests-toolbelt
Пример:
```python
from requests_toolbelt.multipart.encoder import MultipartEncoder, MultipartEncoderMonitor
import requests
from tqdm import tqdm
def upload_multipart_with_progress(url: str, filepath: str):
encoder = MultipartEncoder(fields={'file': ('video.mp4', open(filepath, 'rb'), 'video/mp4')})
bar = tqdm(total=encoder.len, unit='B', unit_scale=True, unit_divisor=1024)
def callback(monitor):
bar.update(monitor.bytes_read - bar.n)
monitor = MultipartEncoderMonitor(encoder, callback)
headers = {'Content-Type': monitor.content_type}
resp = requests.post(url, data=monitor, headers=headers)
bar.close()
resp.raise_for_status()
return resp
```
Когда использовать что:
- Если вам дали presigned URL (часто в ответе API вы получаете конкретный URL для прямой загрузки) — используйте первый способ (PUT/Content-Length).
- Если API просит POST multipart к своему endpoint — используйте второй способ.
Если сервер требует Content-MD5
- Иногда presigned URL или API требует хэш тела (Content-MD5). Нужно посчитать md5 файла и передать в заголовке base64-encoded:
```python
import hashlib, base64
md5 = hashlib.md5()
with open(filepath, 'rb') as f:
for chunk in iter(lambda: f.read(1024*1024), b''):
md5.update(chunk)
content_md5 = base64.b64encode(md5.digest()).decode('ascii')
headers['Content-MD5'] = content_md5
```
Добавьте этот заголовок в запрос, если это требуется.
Диагностика 412
- Посмотрите в документации MAX messenger (или API, который вам дал URL) — какие заголовки обязательны для загрузки.
- Сравните требуемый Content-Type / Content-Length / Content-MD5.
- Логи сервера или тело ответа иногда содержат подсказку о том, какое условие не выполнено.
Если вы пришлёте ваш текущий код (тот самый, который даёт 412) и пример заголовков/ответа от сервера, помогу точнее: укажу, какой заголовок/формат вы упускаете и поправлю код.