Amikor egy videólejátszó a 47. percnél indul el, a szerverednek nem kell hősiesen elküldenie az előtte lévő 46 percet. A kliens Range: bytes=… fejlécet küld, te pedig 206 Partial Content választ adsz. Papíron két fejléc és egy fájlstream. A valóságban pont elég részlet van benne ahhoz, hogy egy „működik nálam” implementáció egy mobilos letöltés-folytatásnál csendben hibás legyen.

A Range nem a letöltés-gyorsítás kapcsolója
A Range request azt mondja meg, hogy a kliens a reprezentáció mely bájtjaira tart igényt. Ennek klasszikus fogyasztója a <video>: előreugráskor új tartományt kérhet. Ugyanez kell letöltés folytatásához, PDF-olvasókhoz és ahhoz, hogy a kliens egy nagy objektumból csak egy kis darabot vizsgáljon meg.
A szerver Accept-Ranges: bytes fejlécben jelezheti a támogatást, de a kliens ettől még küldhet Range-et; a fejléc inkább egyértelmű képességjelzés. Érvényes egyetlen tartományra a válasz 206, és kötelező mellé a Content-Range, például bytes 1000-1999/5000. Az utolsó szám a teljes fájlméret, nem a most küldött ezer bájt. A részleteket az RFC 9110 Range fejezete rögzíti.
A három forma, amelyet a regexed valószínűleg elfelejtett
bytes=500-999: zárt intervallum, mindkét végével.bytes=500-: az 500. bájttól a fájl végéig.bytes=-500: az utolsó 500 bájt. Ez nem a nullától 500-ig tart.
Na, ezt nem tudtam sokáig: a bytes=-500 suffix range, és nem hibás hiányos intervallum. A szervernek az bytes=999999- kérésre, ha a fájl kisebb, 416 Range Not Satisfiable választ kell adnia Content-Range: bytes */<teljes méret> fejléccel. A válaszban nincs fájlrészlet. Ebből tudja a folytatást végző kliens, hová érkezett meg valójában.
Egy szándékosan szűk Node-szerver
Az alábbi példa csak egyetlen bytes tartományt szolgál ki, amit a legtöbb videós és folytató kliens kér. A többrészes kérést (bytes=0-99,200-299) tudatosan teljes 200-zal hagyja figyelmen kívül. Ilyet hibás 206-tal „támogatni” sokkal rosszabb: a helyes válasz ott multipart/byteranges törzset és boundary-kat igényel.
import { createReadStream } from 'node:fs';
import { stat } from 'node:fs/promises';
import http from 'node:http';
const filePath = new URL('./large-file.mp4', import.meta.url);
function parseSingleRange(value, size) {
if (!value || value.includes(',')) return null;
const match = /^bytes=(\d*)-(\d*)$/.exec(value);
if (!match || (!match[1] && !match[2])) return null;
const [, first, last] = match;
let start, end;
if (first === '') {
const length = Number(last);
if (!Number.isSafeInteger(length) || length <= 0) return 'unsatisfiable';
start = Math.max(size - length, 0); end = size - 1;
} else {
start = Number(first);
end = last === '' ? size - 1 : Math.min(Number(last), size - 1);
}
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || start > end || start >= size) return 'unsatisfiable';
return { start, end };
}
http.createServer(async (req, res) => {
const { size } = await stat(filePath);
const common = { 'Accept-Ranges': 'bytes', 'Content-Type': 'video/mp4' };
const range = parseSingleRange(req.headers.range, size);
if (range === 'unsatisfiable')
return res.writeHead(416, { ...common, 'Content-Range': 'bytes */' + size }).end();
if (!range) {
res.writeHead(200, { ...common, 'Content-Length': size });
return createReadStream(filePath).pipe(res);
}
const { start, end } = range;
res.writeHead(206, { ...common, 'Content-Length': end - start + 1,
'Content-Range': 'bytes ' + start + '-' + end + '/' + size });
createReadStream(filePath, { start, end }).pipe(res);
}).listen(3000);
Ne keverd össze az objektum két verzióját
A valódi csapda nem a pozíció, hanem a változó fájl. A kliens letölthette a régi ZIP első felét, te közben újra feltöltötted ugyanarra az URL-re, majd ugyanaz a kliens a második felét kéri. A két fél összeragasztása hibás fájl, amelyet remek lesz hibajegyként kapni.
Erre való az If-Range: a kliens elküldi a korábbi erős ETaget vagy egy megfelelő dátumvalidátort. Egyezéskor jöhet a 206; eltéréskor a szerver figyelmen kívül hagyja a Range-et, és a teljes új reprezentációt küldi 200-zal. Nem 412-t. A resumable download tehát nem csak Range: stabil objektumazonosító vagy ETag is kell hozzá.
Hol legyen ez a logika?
Productionben statikus vagy objektumtárban levő videót, PDF-et és telepítőcsomagot inkább a CDN/reverse proxy rétegére bíznám. Az már kezeli a többrészes range-et, a backpressure-t, a cache-t és a kliens megszakítását; egy Node process ne váljon véletlenül médiakiszolgálóvá. Az alkalmazásból történő, jogosultság-ellenőrzött fájlletöltésnél viszont teljesen indokolt a saját handler. Ilyenkor előbb autorizálj, utána állíts elő egy változatlan, egyértelmű fájlverziót, végül szolgáld ki a range-et.
Nem érdemes Range-t adni minden dinamikus JSON válaszhoz. Nem gyorsítja fel a 30 kB-os API-választ, viszont új cache- és tömörítési kérdéseket visz a rendszerbe. Ott hasznos, ahol a teljes reprezentáció nagy, a részleges olvasás valós felhasználói művelet, és a bájtpozícióknak stabil jelentésük van.