Node Send Partial Content Accept-Ranges Content-Range Range HTTP Headers
Learn to send partial content (HTTP 206) with node including how to use the Accept-Ranges, Content-Range, and Range HTTP headers. Sending partial content is commonly used with streaming video.
Table of Contents 📖
- What is Partial Content?
- Partial Content and Status Code 206
- Headers Required for Partial Content
- Accept-Ranges Header
- Range Header
- Content-Range Header
- Programming Partial Content - Program Summary
- Programming Partial Content - The Client
- Programming Partial Content - The Server
- Running our Program
- Inspecting the Browser Network Tab
- Summary
What is Partial Content?
Partial content is a chunk of an available resource stored on a server. Sending partial content is often used to avoid the overhead of fetching unused resources, as such, it is often used in video streaming. For example, it isn't uncommon for users to watch only part of a video, so why send all the data? Instead, send separate chunks over time and stop if the user leaves.
Partial Content and Status Code 206
When these chunks of data are sent to the client, the HTTP 206 response code is used to indicate that the request was successful and that the response body contains the requested chunk of data. For example, if we requested the first 200 bytes of a text file and received a 206 from the server, the server's response body would contain those 200 bytes of text.
Headers Required for Partial Content
Certain HTTP headers are required to send partial content. HTTP headers let the server and client pass additional information with a request/response. They are case insensitive and follow the format below.
<name>: <value>
For example, a commonly used HTTP header is Content-Type which indicates the media type of the resource being provided.
Content-Type: text/html
Accept-Ranges Header
One required HTTP header for sending partial content is the Accept-Ranges header. The Accept-Ranges header is a response header used by the server to let the client know it supports partial requests for file downloads. It has the following syntax.
Accept-Ranges: <range-unit>
The supplied range unit value defines the range unit that the server supports. Currently, bytes is the only range unit format that is formally defined.
Accept-Ranges: bytes
However, the value none can also be supplied which indicates that no range unit is supported.
Accept-Ranges: none
Range Header
Another required header for sending partial content is the Range HTTP request Header. The Range HTTP header indicates the part of the document that the server should return. It has the following format.
Range: <unit>=<range-start>-<range-end>
Here, unit is the unit in which the range is specified (which is usually in bytes). Range-start is an integer that indicates the beginning of the request range.
Range: bytes=0-199
Note that the numbers in the Range header are inclusive, so bytes 0 and 199 are included. Therefore, the total number would be 200 in this range. The Range header also has multiple formats that can be used. For example, we can specify multiple ranges.
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>, <range-start>-<range-end>, ...
Range: bytes=0-200, 200-400, 400-700
Above, we are saying we want the server to send back three ranges: 0-200, 200-400, and 400-700 bytes. If multiple ranges are specified, the server may send the specified ranges in a multipart document. A multipart document combines one or more sets of data into a single body separated by boundaries. Another format is as follows.
Range: <unit>=<range-start>-
Here, the
Range: <unit>=-<suffix-length>
Range: bytes=-400
Content-Range Header
Another required HTTP header for sending partial content is the Content-Range header. The Content-Range header is a response HTTP header that indicates where in a full body message a partial message belongs. It has the following syntax.
Content-Range: <unit> <range-start>-<range-end>/<size>
Here, unit is the unit the ranges are specified in (usually bytes).
The following Content-Range header says that the bytes in the response are 200-500 of a document that is 50,000 bytes in size.
Content-Range: bytes 200-500/50000
There are variations of the Content-Range header syntax as well. The following syntax can be used for a document whose length is unknown.
Content-Range: <unit> <range-start>-<range-end>/*
The following syntax can be used for a document whose length is known but the range is unknown.
Content-Range: <unit> */<size>
Programming Partial Content - Program Summary
Lets now put what we've learned into practice. We will create the following program below.
Gif created from wittcode-tools.com.
This program consists of a client, a server, and a text file. The client program contains an input field and a button. The input takes a range of bytes that are sent to the server when the button is clicked. The server receives this range of bytes and returns partial content from the text file.
Programming Partial Content - The Client
To start, lets create the client that will request a range of data from the server. Create a file called client.html and place the following inside.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Partial Content Client</title>
</head>
<body>
<label>Range of bytes?</label>
<input id="numBytes" type="text" placeholder="0-200">
<button onclick="sendPartialRequest()">Send Partial Request</button>
<p id="returnedData"></p>
<script>
</ script>
</body>
</html>
This small program has an input asking for a range of bytes, a button to request that range from the server, and a paragraph tag to display the server's response. Now, inside the script tag, lets get some of our HTML elements by their id.
const byteInput = document.getElementById('numBytes');
const returnedData = document.getElementById('returnedData');
Now lets create the function sendPartialContent() that is called when the button is clicked. This function will create and send a request to the server asking for the range of bytes specified inside the input tag.
function sendPartialRequest() {
const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost:1234');
xhr.setRequestHeader('Range', `bytes=${byteInput.value}`);
xhr.send();
}
So, first we create the request to be sent to the server, then we set the request method to GET and the destination to localhost:1234 (which is where our server will be listening). We then set a Range header on the request with the value from the input tag, and then we send off the request. Now lets handle the server's response.
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 206) {
returnedData.innerText = xhr.responseText;
}
}
The onreadyStatechange property of an XHR takes a function that is called whenever the readyState of the XHR changes. For example, the state changes when the XHR is created, sent, finished, etc. When the XHR has finished, the readyState is 4. Also, as we are working with partial content, the server will send back a 206 status code indicating the response contains the partial data. So, when the XHR has finished, and if the status code is 206, we set the text of our paragraph tag to be the partial data.
Programming Partial Content - The Server
Our client is now good to go, so lets work with our server. To begin, lets create a file called server.js, import a few libraries, and declare a few variables.
const http = require('http');
const fs = require('fs');
const port = 1234;
const fileData = fs.statSync('data.txt');
The http module allows us to build an HTTP server. The fs module allows us to interact with the file system. The port variable is the port our server will be listening on. The fs module's statSync() method gets information about a file synchronously (such as the size, creation date, etc.). The file we provide is data.txt which contains the data that will be sent in chunks to the client. Lets create this data.txt file real quick and place the following inside.
This is some data stored in a file that will be read chunk by chunk.
In fact, this data was made by WittCode who is really cool and makes really good videos!!!
Lets now create an HTTP server using the createServer() method of the http module.
const server = http.createServer((req, res) => {
});
The createServer() method takes a callback function that takes two arguments, a request and response. The request is the request received from the client and the response is the response the server will send back to the client. Now, lets work with the Range header that the client sent over and parse out the start and end bytes.
const range = req.headers.range;
const startEnd = range.replace('bytes=', '').split('-');
const [start, end] = startEnd;
First, we obtain the range header from the request's headers property. We then remove the bytes= part of the string with the string replace() method and split the resulting string into an array containing the start and end bytes. Now, lets add some headers to our server response, along with the status code to indicate this response contains partial content.
res.writeHead(206, {
'Content-Type': 'text/plain',
'Access-Control-Allow-Origin': '*',
'Accept-Ranges': 'bytes',
'Content-Range': `bytes ${start}-${end}/${fileData.size - 1}`,
'Content-Length': `${end - start + 1}`
});
The writeHead() method writes the header of the response that our server sends to the client. The first argument is the HTTP status code. As we are working with partial content, we send a 206. The Content-Type header tells the client that the response type is plain text. We also need to add the Access-Control-Allow-Origin header as our client is on localhost:5050 while our server is on localhost:1234. This is a cross-origin request so it will be denied by the CORS policy unless we specify all origins that are allowed to make requests.
Next, we let the client know that we accept the Ranges header in the form of bytes. Then, we use the start and end bytes sent over from the client, along with the size of the data.txt file (we subtract 1 because with the Range header byte 0 is included), to specify which bytes are being returned. Finally, we include the Content-Length header to let the client know how many bytes of data were sent over. As the bytes are inclusive in Content-Range, we need to tag on a +1. Now, lets create a stream to read the specified range of bytes from our text file.
const fileStream = fs.createReadStream('data.txt', { start: Number(start), end: Number(end) });
We create this read stream using the fs module's createReadStream() method. This method takes an options object where we can specify the start and end bytes we want to read. We use the start and end bytes from the client's Range header to determine the start and end. We also need to convert them to numbers as strings are not allowed as properties for start and end. Now we just need to send this stream of data to our client.
fileStream.pipe(res);
The pipe() method takes a readable stream and pipes it to a writeable stream. A writeable stream is simply an abstraction for a destination to which data is written. HTTP responses on the server are writeable streams. Also note that pipe() automatically ends the writer stream when the reader ends. Finally, we just need to make our server listen on a specified port.
server.listen(port, () => {
console.log(`Listening on port: ${port}...`);
});
This listen() method also takes a callback function. In this callback function we simply log to the console the port that the server is running on.
Running our Program
To run our server.js file we do the following.
node server.js
To run our client.html file, we use the VS Code live server extension. Right click on the file and click start with live server.
Inspecting the Browser Network Tab
We can observe the exchange of partial data between our server and client over the network. Take a look at the response and request headers!
Network tab indicating our partial content exchange with the server. Notice the 206 status code and the relevant headers.
Summary
But there we have it! Partial content is very useful for reducing the overhead associated with resource delivery. If this article was useful, please consider donating, sharing this article, and subscribing to my YouTube channel WittCode!