Ever paused a download, to later resume it? Or played back a video from a specific point, without downloading the whole video?
You already used HTTP range requests without even realizing it.
Recently had the chance to use them in a personal project, my-yt
, and I learned a ton, so here we go.
In a nutshell
In simple terms: HTTP range requests allow clients to ask for only a portion of the resource, given a start and end byte marker.
How? Using the HTTP Range
header.
E.g. Range: bytes=0-2048
means “give me bytes from 0 to 2048”. You can also request multiple ranges and a resource given only a starting range.
The server then send back the requested resource portion. It does that using the Content-Range
HTTP header to inform the client.
There is also the Accept-Ranges
used by the server to tell the client that range requests are supported.
And have you ever heard about the HTTP status code 206
? It’s the one used when a server sends back a portion of the resource, like in this case.
(BTW, there is waaaaay more stuff to get into when talking about this, so here I am only scratching the surface)
Show me some code
A basic example (to see the full code, check out my-yt)
The client (e.g. a video player in your HTML) requests the first X bytes of a video file.
The server then send back the requested resource portion. As mentioned above, some HTTP headers actions comes into play here.
E.g.
The HTTP function handler responsible for replying with the correct chunk of the video:
function watchVideoHandler (req, res) {
const location = `./your/video.mp4`
const contentType = 'video/mp4'
if (!fs.existsSync(location)) {
res.writeHead(404, { 'Content-Type': 'text/plain' })
res.end('Video not found')
return
}
res.setHeader('content-type', contentType)
Parse the requested HTTP range
const options = {}
let start
let end
const range = req.headers.range
if (range) {
const bytesPrefix = 'bytes='
if (range.startsWith(bytesPrefix)) {
const bytesRange = range.substring(bytesPrefix.length)
const parts = bytesRange.split('-')
if (parts.length === 2) {
const rangeStart = parts[0] && parts[0].trim()
if (rangeStart && rangeStart.length > 0) {
options.start = start = parseInt(rangeStart)
}
const rangeEnd = parts[1] && parts[1].trim()
if (rangeEnd && rangeEnd.length > 0) {
options.end = end = parseInt(rangeEnd)
}
}
}
}
Determine the file size and return it if its a HEAD request
const stat = fs.statSync(location)
const contentLength = stat.size
if (req.method === 'HEAD') {
res.statusCode = 200
res.setHeader('accept-ranges', 'bytes')
res.setHeader('content-length', contentLength)
return res.end()
}
Calculate the bytes that are being sent back to the client
let retrievedLength = contentLength
if (start !== undefined && end !== undefined) {
retrievedLength = end - start + 1
} else if (start !== undefined) {
retrievedLength = contentLength - start
}
res.statusCode = (start !== undefined || end !== undefined) ? 206 : 200
res.setHeader('content-length', retrievedLength)
if (range !== undefined) {
res.setHeader('accept-ranges', 'bytes')
res.setHeader('content-range', `bytes ${start || 0}-${end || (contentLength - 1)}/${contentLength}`)
}
Pipe the file stream to the response
const fileStream = fs.createReadStream(location, options)
fileStream.on('error', error => {
console.error(`Error reading file ${location}.`, error)
res.writeHead(500)
res.end()
})
fileStream.pipe(res)
}