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

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

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

Коротко

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

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

Если 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, должен переспросить целиком, а не решить, что файл пропал.

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

Сжатие и куски несовместимы. Тут никакой настройки «включить и то и другое» нет: сжатый поток - другие байты, и смещения в нём не совпадают с исходными. На практике это значит: для текстовых файлов выбирают сжатие (они мелкие, докачивать нечего), а для архивов, видео и дистрибутивов - куски. Хорошая новость в том, что такие файлы и так сжаты внутри, и сжимать их второй раз бессмысленно.

Проксируемый ответ 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, где указаны границы и полный размер. nginx ставит Accept-Ranges: bytes сам, и по этому заголовку клиент понимает, что докачка возможна. Несколько кусков за раз дают multipart/byteranges, выход за границу - 416. Сжатие отменяет куски целиком: для текста выбирают сжатие, для архивов и видео - куски. Проксируемый ответ режет не nginx, а приложение.

Комментарии

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

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

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