Загрузка файла оборвалась: поток HTTPX, временный файл и закрытие ответа

Загрузчик получает большой файл и сразу пишет response.content в конечный путь. В этом варианте весь ответ сначала оказывается в памяти. Если перейти к записи кусками прямо в конечный файл, появится другая проблема: после обрыва сети потребитель увидит недокачанный результат как готовый. Нужно одновременно определить чтение потока, закрытие ответа и момент публикации файла.

В HTTPX потоковый ответ удобно открывать через async with client.stream. Внутри сначала проверяем статус через raise_for_status, затем читаем aiter_bytes. Контекст закроет ответ и при исключении. На диск пишем уникальный временный файл рядом с назначением, а конечное имя заменяем лишь после успешного завершения чтения.

Чтение до публикации файла

Пример использует MockTransport, поэтому копируется без внешнего сервера. Функция download получает уже открытый клиент: вызывающий управляет его временем жизни. TemporaryDirectory изолирует проверку, а до вызова в конечном пути лежит старая версия. После успеха там должно оказаться новое содержимое.

import asyncio
import os
import tempfile
from pathlib import Path
import httpx

async def download(client, url: str, destination: Path):
    temporary = None
    try:
        async with client.stream("GET", url) as response:
            response.raise_for_status()
            with tempfile.NamedTemporaryFile(dir=destination.parent, delete=False) as stream:
                temporary = Path(stream.name)
                async for chunk in response.aiter_bytes():
                    stream.write(chunk)
        os.replace(temporary, destination)
    finally:
        if temporary is not None:
            temporary.unlink(missing_ok=True)

async def main():
    transport = httpx.MockTransport(lambda request: httpx.Response(200, content=b"new"))
    with tempfile.TemporaryDirectory() as directory:
        destination = Path(directory) / "download.bin"
        destination.write_bytes(b"old")
        async with httpx.AsyncClient(transport=transport) as client:
            await download(client, "https://files.invalid/data", destination)
        assert destination.read_bytes() == b"new"
        print("downloaded")

asyncio.run(main())

Обрыв, размер и время загрузки

Установите HTTPX: python -m pip install httpx. На Python 3.12 пример печатает downloaded. Для отрицательного теста передайте поток, который после первой порции поднимает httpx.ReadError. Старый конечный файл должен сохраниться, временный — исчезнуть. Отдельно проверьте 404: содержимое страницы ошибки не должно стать загруженным документом.

Обычная запись в файл синхронна. В примере она короткая, но на медленном диске или при больших блоках может задерживать event loop. Для такого окружения вынесите дисковые операции в подходящий исполнитель или используйте файловый API, принятый в проекте. При этом сохраните порядок: запись всех порций, закрытие файла, публикация, уборка при отказе.

aiter_bytes возвращает декодированное содержимое после обработки HTTP content encoding. Если задача состоит в сохранении исходного сжатого потока, разберите aiter_raw и ожидаемый формат. Имя метода здесь влияет на то, какие именно байты попадут в файл. Для проверки целостности используйте контрольную сумму или размер согласно договорённости с источником, учитывая выбранное представление.

Streaming ограничивает необходимость держать весь ответ в памяти, но не задаёт предел размера загрузки. Если вход может быть большим, считайте фактически полученные байты и прерывайте операцию после лимита. Заранее определите также общий бюджет времени. Полезный загрузчик публикует завершённый результат, освобождает ответ и оставляет понятный отказ вместо частичного файла.

Когда локальный пример работает, полезно разобрать соседние механизмы языка и их взаимодействие. В курсе «Python для профессионалов» есть практика по итераторам, генераторам, декораторам, asyncio, метапрограммированию и профилированию. Эти темы помогают понимать время жизни объектов, управление выполнением и причины поведения прикладной программы.

Посмотреть программу и практику курса.

Документация: официальный источник по теме.

Другие задачи по теме: База, HTTP-клиенты и тесты API.

Продолжить на практике

  • Python для профессионалов ↗

    Python для профессионалов — курс для людей, стремящихся углубить свои знания языка. Вы изучите итераторы, генераторы, декораторы, современные возможности Python (3.8+), освоите метапрограммирование, асинхронность, внутреннее устройство интерпретатора, инструменты оптимизации и профилирования, а также получите уверенность в использовании продвинутых возможностей стандартной библиотеки.