Докачка работает только тогда, когда клиент запрашивает оставшуюся часть того же файла, а сервер возвращает именно этот диапазон. Минимальный признак исправного сценария — ответ 206 Partial Content с согласованным заголовком Content-Range; одного Accept-Ranges: bytes недостаточно.
Если на запрос с Range приходит 200 OK, нельзя безусловно дописывать тело к уже сохранённому фрагменту: сервер мог проигнорировать диапазон или файл изменился. Сначала проверьте поведение на одном стабильном публичном URL, затем сравните origin, прокси и CDN.
Как устроена докачка файла
Допустим, клиент уже сохранил первые 1 048 576 байт. Следующий GET-запрос содержит:
~~~http Range: bytes=1048576- ~~~
Для неизменившегося файла сервер возвращает 206, тело начиная с указанной позиции и заголовок вида:
~~~http Content-Range: bytes 1048576-5242879/5242880 ~~~
Числа означают начальный и конечный байты включительно, затем полный размер представления. Клиент обязан сопоставить их со своей локальной копией, а не ориентироваться только на код ответа.
Поддержка диапазонов в HTTP необязательна. Accept-Ranges: bytes сообщает о поддержке, но его отсутствие не доказывает обратное: решающим остаётся реальный GET с Range. Код 416 Range Not Satisfiable обычно означает, что запрошенное начало находится за текущим концом файла либо диапазон неприменим; в ответе полезно проверить Content-Range: bytes */полный_размер.
Проверка Range и 206 через curl
Используйте тестовый публичный файл без персональных данных, cookie и временной подписанной ссылки. Сначала сохраните базовые заголовки:
~~~bash curl -sS -D - -o /dev/null -H 'Accept-Encoding: identity' 'https://example.ru/files/archive.zip' ~~~
Зафиксируйте Content-Length, ETag, Last-Modified, Content-Encoding и Accept-Ranges. Заголовок Accept-Encoding: identity убирает влияние сжатия из этого лабораторного теста; отдельно повторите запрос с теми же заголовками, которые отправляет реальный клиент.
Теперь запросите первые 100 байт:
~~~bash curl -sS -D /tmp/range.headers -o /tmp/range.part -H 'Range: bytes=0-99' -H 'Accept-Encoding: identity' 'https://example.ru/files/archive.zip' ~~~
Проверьте три условия:
- статус — 206 Partial Content;
- Content-Range — bytes 0-99/полный_размер;
- в теле ровно 100 байт.
Затем запросите диапазон от реальной точки остановки до конца. Если сервер возвращает 200, это ещё не всегда неисправность: Range можно не поддерживать, а при неуспешном If-Range полный ответ обязателен. Ошибка клиента начинается тогда, когда он принимает такой ответ за продолжение и склеивает два полных файла.
После того как известен полный размер, запросите начало за его пределами. Ожидаемый результат для поддерживающего диапазоны ресурса — 416 с текущим полным размером в Content-Range. Не используйте произвольное огромное число до получения размера: файл действительно может оказаться больше предположения.
Зачем нужен If-Range
Между первой загрузкой и докачкой файл способен измениться. Тогда старый фрагмент нельзя соединять с новой версией. If-Range связывает диапазон с валидатором исходного ответа — предпочтительно с сильным ETag:
~~~bash curl -sS -D - -o /tmp/resumed.part -H 'Range: bytes=1048576-' -H 'If-Range: "etag-iz-pervogo-otveta"' -H 'Accept-Encoding: identity' 'https://example.ru/files/archive.zip' ~~~
Если валидатор совпал и диапазон применим, ожидайте 206. Если файл изменился, сервер игнорирует Range и возвращает полный 200 OK: клиент должен заменить локальную копию или начать загрузку заново. Слабый ETag с префиксом W/ для If-Range не подходит; при отсутствии сильного ETag можно использовать Last-Modified, понимая меньшую точность проверки по дате.
Полезный контрольный тест — намеренно передать заведомо другой ETag. Исправная цепочка должна вернуть полный ответ, а не фрагмент новой версии под видом продолжения старой.
Где чаще ломается докачка
Проверяйте путь по слоям, не меняя сразу несколько настроек:
- приложение или файловый обработчик не поддерживает Range и всегда отдаёт 200;
- reverse proxy либо CDN удаляет Range, неправильно кэширует ответ или меняет Content-Range;
- разные узлы отдают файл разного размера либо с разными валидаторами;
- динамический архив пересобирается между запросами, поэтому это уже другое представление;
- подписанная ссылка истекает, а повторный запрос попадает в авторизацию или редирект;
- клиент запрашивает диапазон от неверной позиции после локального повреждения;
- сервер отвечает 206, но длина тела не соответствует заявленному диапазону.
Сначала выполните одинаковый запрос к публичному адресу через CDN и, только если у вас есть безопасный административный маршрут, к origin. Не публикуйте origin IP, закрытые URL, токены и cookie в сторонних проверочных сервисах.
Как проверить реальное возобновление
curl подтверждает протокол, но финальный тест должен повторять пользовательский сценарий:
- начните загрузку стабильного файла и прервите её;
- зафиксируйте размер локального фрагмента, URL и валидатор;
- возобновите загрузку и найдите запрос с Range в сетевом журнале клиента;
- сопоставьте статус, Content-Range и фактически полученное число байт;
- сравните итоговый размер, а для неизменяемого файла — известную контрольную сумму;
- повторите через тот же CDN, авторизацию и тип сети, где возник сбой.
Не делайте вывод по одному успешному короткому диапазону: он не проверяет истечение ссылки, смену узла или изменение файла во время долгой передачи.
Что мониторить после исправления
Обычная проверка доступности URL замечает, что файл перестал отвечать, но не заменяет синтетический тест докачки с пользовательскими заголовками. В Web-Puls можно наблюдать доступность публичного URL и время ответа, а проверку Range стоит оставить отдельным сценарием в тестах релиза.
Если 206 теряется между приложением, nginx и CDN или валидаторы расходятся на разных узлах, соберите URL, время с часовым поясом, безопасные заголовки и результаты двух Range-запросов. С этим набором можно отправить заявку на профессиональную диагностику: специалисту будет проще локализовать слой, не запрашивая пароль или закрытую ссылку.
Короткий критерий готовности
Докачка настроена корректно, если стабильный файл отвечает 206 на допустимый Range, возвращает точный Content-Range, отклоняет недопустимую позицию через 416, а If-Range не позволяет склеить разные версии. После этого проверьте тот же сценарий реальным клиентом через production-прокси или CDN.