1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6

# теория · шаг 3 из 6

Range: докачка и перемотка

Коротко

Клиент вправе попросить не весь файл, а кусок: с такого-то байта по такой-то. На этом стоят докачка после обрыва и перемотка видео - и обе ломаются от одной безобидной настройки.

  • Просьба - заголовок Range, ответ - код 206 и Content-Range с границами и полным размером.
  • nginx сам говорит Accept-Ranges: bytes на каждый файл с диска. Послать Range клиент вправе и без него, но менеджеры загрузок и браузеры по его отсутствию обычно решают, что докачки нет.
  • Кусков можно попросить несколько сразу: ответ станет multipart/byteranges.

Если 206, Content-Range и связка со сжатием знакомы - листай до «Теперь сам».

Сначала ответь сам

Пользователь качает дистрибутив на два гигабайта, связь рвётся на восьмидесяти процентах. Он нажимает «продолжить» - и загрузка начинается с нуля.

Файл лежит на диске, nginx настроен обычным образом. Почему клиент не смог докачать?

Потому что кто-то включил сжатие на этот тип файлов (gzipРазбирается в разделе 11, глава «gzip и brotli»: Что сжимать и чем - nginx пережимает ответ на лету, если браузер сказал, что умеет его распаковывать). Проверено на стенде: тот же файл в 300 байт, тот же запрос с Range (-r 0-9 в curl - «дай байты с 0 по 9»), разница только в заголовке Accept-Encoding: gzip. Браузер присылает его сам всегда, из curl его добавляют через -H:

$  команда
curl -sI -r 0-9 localhost/f.txt
↳  выводcurl -sI -r 0-9 localhost/f.txt
HTTP/1.1 206 Partial Content
Server: nginx/1.31.5
Date: Thu, 10 Sep 2026 19:53:00 GMT
Content-Type: text/plain
Content-Length: 10
Last-Modified: Thu, 03 Sep 2026 11:46:11 GMT
Connection: keep-alive
ETag: "6a995e03-12c"
Content-Range: bytes 0-9/300
$  команда
curl -sI -r 0-9 -H 'Accept-Encoding: gzip' localhost/f.txt
↳  выводcurl -sI -r 0-9 -H 'Accept-Encoding: gzip' localhost/f.txt
HTTP/1.1 200 OK
Server: nginx/1.31.5
Date: Thu, 10 Sep 2026 19:53:00 GMT
Content-Type: text/plain
Last-Modified: Thu, 03 Sep 2026 11:46:11 GMT
Connection: keep-alive
ETag: W/"6a995e03-12c"
Content-Encoding: gzip

Во втором случае вместо куска пришёл весь файл, сжатый, с кодом 200. И на обычный запрос без Range сжатый ответ приходит без Accept-Ranges, хотя несжатый его несёт - замер показал оба. Сервер честно сообщает: кусками этот ответ не отдаю.

На что это похоже

Библиотека выдаёт книгу двумя способами.

Обычный - книгу целиком: пришёл, взял, унёс. Если по дороге уронил в лужу, возвращаешься и берёшь новую целиком.

Второй - по страницам: «мне со сто сорок первой по двухсотую». Так можно дочитать ровно с того места, где остановился, и не тащить весь том. Но работает это, только пока книга нумерована.

А теперь представь, что перед выдачей текст пересказывают своими словами покороче. Пересказ сам по себе полезен, но нумерация страниц в нём другая, и просьба «мне сто сорок первую» смысла больше не имеет. Ровно это делает сжатие: оно меняет байты, и старые номера перестают на что-либо указывать.

Механизм: просьба и ответ

клиент просит Range: bytes=1000-1999 «с тысячного байта по 1999-й» сервер отвечает 206 Partial Content Content-Range: bytes 1000-1999/50000 кусок, границы и полный размер Клиент узнаёт о такой возможности заранее, из обычного ответа: Accept-Ranges: bytes этот заголовок nginx ставит сам на любой файл с диска. Нет его в ответе - клиенты обычно не пробуют докачать.
Три заголовка на весь механизм. Всё остальное - следствия: перемотка видео, докачка, параллельная загрузка кусками.

Правило

Куски возможны, пока байт с номером N в ответе - это байт с номером N в файле. Всё, что меняет содержимое на лету, эту связь рвёт.

Разбор: четыре ответа на четыре просьбы

$  команда
for r in 0-9 0-9,20-29 99999- 290-; do echo "== Range: bytes=$r"; curl -s -o /dev/null -D- -r $r localhost/f.txt | grep -E 'HTTP|Content-Range|Content-Length|Content-Type'; done
↳  выводfor r in 0-9 0-9,20-29 99999- 290-; do echo "== Range: bytes=$r"; curl -s -o /dev/null -D- -r $r localhost/f.txt | grep -E 'HTTP|Content-Range|Content-Length|Content-Type'; done
== Range: bytes=0-9
HTTP/1.1 206 Partial Content
Content-Type: text/plain
Content-Length: 10
Content-Range: bytes 0-9/300
== Range: bytes=0-9,20-29
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=00000000000000000001
Content-Length: 218
== Range: bytes=99999-
HTTP/1.1 416 Requested Range Not Satisfiable
Content-Type: text/html
Content-Length: 197
Content-Range: bytes */300
== Range: bytes=290-
HTTP/1.1 206 Partial Content
Content-Type: text/plain
Content-Length: 10
Content-Range: bytes 290-299/300

-o /dev/null выбрасывает тело, -D- печатает заголовки ответа на экран. Байты считаются с нуля, и обе границы входят в кусок: 0-9 - это десять байт, а 290- - «с 290-го до конца».

Что стоит заметить.

Полный размер файла всегда в ответе - после дроби в Content-Range. Клиент узнаёт его из первого же куска и дальше считает прогресс сам.

Несколько кусков за раз - это другой формат тела. multipart/byteranges складывает куски с разделителями, как вложения в письме; boundary - и есть строка-разделитель между кусками. Так браузер тянет шрифты и метаданные видео: два-три разных места файла одним запросом.

Выход за границу - 416, а не 404. В ответе Content-Range: bytes */300 - «такого куска нет, а файл длиной 300». Разница смысловая: файл есть, а такого куска в нём нет. Клиент, получив 416, должен переспросить целиком, а не решить, что файл пропал.

Разбор: где наивное правило подводит

Сжатие на лету и куски несовместимы. Тут никакой настройки «включить и то и другое» нет: сжатый поток - другие байты, и смещения в нём не совпадают с исходными. Исключение - заранее сжатый файл из gzip_static: его длина известна, и на стенде nginx 1.31.6 запрос Range: bytes=0-9 к нему вернул 206 с Content-Encoding: gzip и десятью байтами СЖАТОГО файла. Докачать таким способом можно, но только тот же .gz и только клиентом, который это понимает. На практике это значит: для текстовых файлов выбирают сжатие (они мелкие, докачивать нечего), а для архивов, видео и дистрибутивов - куски. Хорошая новость в том, что такие файлы и так сжаты внутри, и сжимать их второй раз бессмысленно.

Проксируемый ответ nginx кусками не режет. Заголовок Range он передаст бэкенду, а дальше всё зависит от приложения: умеет - вернёт 206, не умеет - вернёт 200 и весь файл. Проверено: тот же запрос к статике даёт 206, а к бэкенду 200. Если раздачу больших файлов делает приложение, докачка становится его заботой - или файлы отдают напрямую с диска, минуя его.

Куски можно и запретить. max_ranges 0; (пишется в http, server или location; число - сколько кусков разрешено в одном запросе) - и nginx перестаёт понимать Range, убрав заодно Accept-Ranges. Директиву иногда ставят против клиентов, которые дробят загрузку на сотни запросов и создают лишнюю нагрузку. Проверено: ответ становится обычным 200.

Что ломается без этого

Первое - «докачка не работает, и непонятно почему». Пока не знаешь про связку со сжатием, ищешь причину в клиенте, в сети, в правах на файл. А достаточно посмотреть, есть ли в ответе Accept-Ranges.

Второе - видео, которое не перематывается. Плеер перематывает ровно Range-запросами: просит кусок с нужной секунды. Нет кусков - нет перемотки, только последовательное воспроизведение с начала.

Зачем это в работе

Проверка занимает секунду:

$  команда
curl -sI https://shop.local/video/intro.mp4 | grep -i accept-ranges

Есть строка - докачка и перемотка работают. Нет - смотри, не попал ли этот тип файлов под gzip_types (тип nginx берёт по расширению из mime.types) и не стоит ли где-нибудь max_ranges 0.

Попросить кусок руками тоже полезно, особенно на большом файле:

$  команда
curl -s -r 0-99 -o /dev/null -D- https://shop.local/dist/app.tar.gz | grep -i content-range

Флаг -r в curl - это и есть Range. Ответ с Content-Range подтверждает, что сервер умеет отдавать куски, и заодно показывает полный размер файла, скачав только эти сто байт: Content-Range: bytes 0-99/300.

Теперь сам

Раздача обновлений: файлы по 300 мегабайт, клиенты на мобильной связи, обрывы частые. В конфиге для этого каталога стоит gzip on; и gzip_types со всеми типами подряд. Что произойдёт и что менять?

Докачки не будет: ответ, сжатый на лету, отдаётся только целиком, и клиент после обрыва начнёт заново - на мобильной связи это может не закончиться никогда. Сжатие тут и не нужно: обновления почти наверняка уже в архиве, и второй проход даёт проценты. Убрать эти типы из gzip_types, проверить, что вернулся Accept-Ranges: bytes, - и клиенты начнут дотягивать хвост вместо повторной загрузки.

Главное

Для докачки клиенту нужен корректный ответ на Range: код 206, границы части и полный размер в Content-Range. Проверь фактический ответ после включения сжатия или прокси. Доступность файла по обычному GET ещё не доказывает, что сервер отдаёт его части.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.