Частичная передача ресурса
Загрузка из предыдущего урока создала отдельный файл. Теперь представим, что клиент получил только его начало и хочет продолжить передачу. Повторное скачивание целиком может быть дорогим, однако простое соединение старого начала с новым концом опасно: за это время представление могло измениться.
В этом уроке согласуем частичную передачу и условие продолжения. Результат — вы сможете объяснить 206, 416 и обычный 200 при неподходящей версии. Используем авторский обмен, не сетевой захват. Файл f001 принадлежит Alice u17 и содержит ровно ASCII ABCDEFGHIJKL, двенадцать байтов без перевода строки. В архиве этот порядок сохранён отдельным текстовым файлом.
Диапазон относится к байтам представления
Идентификатор f001 обозначает ресурс, а Range выбирает участок его текущего представления. В нашем узком договоре передаётся исходный текст без сжатия, с сильным ETag "f001-identity-v1". Если инфраструктура добавит сжатие, границы и валидатор придётся согласовать для соответствующего кодированного представления, а не продолжать считать байты несжатого текста.
Позиции начинаются с нуля. Символ A находится на позиции 0, D — на 3, L — на 11. Последняя граница включается. Поэтому диапазон 0–3 содержит четыре байта. Для русского UTF-8 текста граница могла бы оказаться внутри многобайтового символа; сервер передаёт байты, а сборка и последующее декодирование полного текста являются отдельной работой клиента.
GET /api/files/f001 HTTP/1.1
Host: catalog.example
Authorization: Bearer tok-alice
Range: bytes=0-3
Права проверяются до выдачи данных. Публичный курс c001 не превращает его закрытый файл в общедоступный. Alice владеет f001; другой пользователь получает выбранный нами 404. Маленькая часть файла всё равно может содержать секрет, поэтому Range не создаёт исключения из проверки owner.
Успешная часть имеет собственную длину
Предполагаемый ответ на приведённый запрос:
HTTP/1.1 206 Partial Content
Content-Type: text/plain; charset=utf-8
Content-Disposition: attachment; filename="catalog.txt"
X-Content-Type-Options: nosniff
Cache-Control: private, no-store
Accept-Ranges: bytes
ETag: "f001-identity-v1"
Content-Range: bytes 0-3/12
Content-Length: 4
ABCD
Content-Range описывает место части внутри полного представления: начало 0, конец 3, общий размер 12. Content-Length относится только к телу данного ответа и равен четырём. Нельзя подставить сюда полный размер файла: получатель ожидал бы ещё восемь байтов, которых в этом ответе нет.
Accept-Ranges сообщает поддержку единицы bytes. Это полезная подсказка, а не обязательство каждого сервера выполнить любой Range. Даже поддерживающий диапазоны ресурс может отвечать иначе при неподходящем условии, ошибке доступа или собственной допустимой политике обработки. Клиент читает фактический статус и поля ответа, а не достраивает их из старой подсказки.
В HTTP-листингах перевод строки после ABCD служит границей оформления. Он не добавляется к учебным четырём байтам. Фрейминг HTTP/1.1 сокращён; HTTP/2 и HTTP/3 имеют другое транспортное кодирование, хотя смысл Range и Content-Range сохраняется.
Запрос последних байтов
Иногда клиент знает, что ему нужен конец, но не знает полной длины. Суффиксный диапазон записывается иначе:
GET /api/files/f001 HTTP/1.1
Host: catalog.example
Authorization: Bearer tok-alice
Range: bytes=-4
Здесь минус не означает отрицательный индекс. Он задаёт число последних байтов. Для нашего файла ожидается 206 с Content-Range: bytes 8-11/12, длиной четыре и телом IJKL. Такая форма полезна, например, при чтении небольшого служебного хвоста, но формат конкретного файла может вообще не позволять интерпретировать хвост отдельно.
Открытая форма bytes=4- означает передачу от позиции 4 до конца. Для исходных двенадцати байтов это EFGHIJKL. Если запрошенный конец выходит за длину, допустимая часть может быть ограничена последним доступным байтом. Не следует путать такую ситуацию с началом, которое уже лежит за всем представлением.
Когда диапазон неудовлетворим
Запрос bytes=12- начинается после последней позиции 11. В нашем сценарии нет ни одного подходящего байта. Выбираем отказ:
HTTP/1.1 416 Range Not Satisfiable
Content-Range: bytes */12
Content-Type: application/problem+json
Cache-Control: no-store
{"type":"https://catalog.example/problems/range-unsatisfied","title":"Диапазон недоступен","status":416,"detail":"Выбранное представление содержит 12 байтов."}
Звёздочка не задаёт часть. Она сообщает, что удовлетворимый диапазон не передан, и позволяет узнать текущий полный размер. Тело здесь описывает ошибку, а не фрагмент файла. Клиенту стоит сверить размер и сохранённую версию, прежде чем повторять запрос с механически уменьшенным индексом.
Наш учебный сервер поддерживает один диапазон bytes. Неизвестную единицу, несколько диапазонов и неразбираемый Range он игнорирует и обрабатывает GET как полный запрос. Это выбранная ограниченная политика, а не утверждение, что HTTP вообще не поддерживает multipart-ответы с несколькими частями. В реальном контракте различие между игнорированием и отказом нужно описать явно.
Продолжение требует подходящей версии
Пусть клиент уже сохранил ABCD и сильный ETag. Для остатка он передаёт оба поля:
GET /api/files/f001 HTTP/1.1
Host: catalog.example
Authorization: Bearer tok-alice
Range: bytes=4-
If-Range: "f001-identity-v1"
При совпадении сильного валидатора ожидается 206 с позиций 4–11. Клиент может соединить часть со своим сохранённым началом, поскольку условие связывает обе передачи с одним представлением. Сам ETag не является контрольной суммой, которую клиент обязан вычислять: его значение задаёт сервер.
Если If-Range не совпал, сервер игнорирует Range и возвращает обычный 200 с полным текущим представлением. Это не 412 по нашему прежнему If-Match сценарию. Смысл условия другой: «дай остаток прежней версии, иначе пришли целиком новую». Получив 200, клиент заменяет старую частичную копию, а не приклеивает полный новый файл к её началу. Правила Range и If-Range в RFC 9110.
Почему слабый ETag не подходит
Слабое совпадение допускает семантическую эквивалентность без полной побайтовой одинаковости. Для сборки частей этого недостаточно. Клиент не должен формировать If-Range со слабым тегом W/"...". В нашем договоре используются только сильные теги фиксированной выдачи; вариант If-Range с датой здесь не вводится, поскольку он требует дополнительных условий сильного валидатора времени.
ETag HTML-страницы курса тоже не подходит файлу. Даже если оба ресурса изменились одновременно, их теги обозначают разные выбранные представления. Аналогично тег сжатого файла нельзя переносить на несжатую выдачу без соответствующего серверного договора.
HEAD позволяет получить метаданные без тела, однако Range предназначен для GET и не превращает HEAD в передачу участка. Наш HEAD сообщает свойства доступного полного файла и не выдаёт ABCD. Условные проверки и ошибки доступа при этом остаются значимыми.
Частичная копия хранит и свою границу
Клиенту недостаточно записать только «получил четыре байта». Нужно знать, что это позиции 0–3 определённого представления f001, и сохранить подходящий сильный валидатор. Четыре последних байта IJKL не являются таким же началом, хотя имеют ту же длину.
Если локальные части пересекаются, программа сопоставляет их позиции и версию, а не просто соединяет строки в порядке прихода ответов. Если между ними есть пробел, полный файл ещё не собран. Такие требования особенно заметны при параллельном скачивании, но наш договор не вводит сложный многопоточный клиент. Он даёт минимальные сведения, без которых даже последовательное продолжение может оказаться неверным.
Как читать будущий опыт
При отдельной реализации сопоставляйте четыре вещи: фактический статус, границы, длину тела и валидатор. Одного 206 недостаточно: неправильный Content-Range способен испортить собранный файл. Несовпавший If-Range должен привести к замене копии полным ответом, а отказ доступа — к отсутствию любого содержимого.
Здесь эти запросы не отправлялись и побайтовая передача не проверялась. Мы получили точный договор, который можно перенести в будущую реализацию. Следующий урок рассматривает другой способ получить ресурс: переход по Location, где важно сохранить смысл метода и не переслать credentials чужому origin.