Back to blog
May 2018·3 min readAI-translated

Handling HTTP Range Requests with PHP

#downloadmanagement#php#range-request

Sometimes we don't want to load the entire file from the server, but only a certain section (range) of it. So we want to define a start byte and an end byte and return all data in that range. This is useful for partial downloads (= when we want to resume a download that has already started, or want to build download management via PHP) or in video streaming, when we don't want to load the whole video from the beginning but only the relevant part that is played next (or the part we just jumped to). We achieve this by creating a range request.

1. Request

For this we add the following header to the request:

Range: bytes=STARTBYTE-ENDBYTE

so that the server knows which section to deliver. This only works like this if we access the file directly, i.e. the server manages the download itself and handles the file. If the request has to pass through PHP first, we need to write our own script for it.

2. Server - Basics / reading the range

In the first step we have to find out which range should be returned. There are several ways to do that, but the simplest is to extract the range via regex.

if (isset($_SERVER['HTTP_RANGE'])) {
  $range = preg_match('/^bytes=((\d*-\d*,? ?)+)$/', $_SERVER['HTTP_RANGE'], $matches) ? $matches[1] : [0, filesize("video.mp4")];
}

$range[0] now contains the first byte we have to return. $range[1] contains the last byte we have to return. If the Range header wasn't set in the request, we return the whole file - so we start at byte 0 and end at the file size in bytes, which we can find out with filesize(). Now we still have to read the file, between the start byte and the end byte.

$fp = fopen('video.mp4', 'r');
fseek($fp, $range[0]);
echo fgets($fp, $range[1] - $range[0]);

Here we open the file into the variable "fp". fseek sets the cursor to the first byte position we have to return, and fgets returns the number of bytes we need to read. It's important that as the second parameter of fgets we don't pass the end byte itself, but the number of bytes to read (= end byte minus start byte). Now we can send a request with a header in the format

Range: bytes=startbyte-endbyte

3. Server - Advanced

The specification at tools.ietf.org describes further ways to request a specific range, though. The relevant part looks like this:

The first 500 bytes:
Range: bytes=0-499

The second 500 bytes:
Range: bytes=500-999

All bytes except for the first 500 until the end of document:
Range: bytes=500-

The last 500 bytes of the document:
Range: bytes=-500

Two separate ranges:
Range: bytes=50-99,200-249

The first 100 bytes, 1000 bytes starting from the byte number 500,
and the remainder of the document starting from byte number 4000:
Range: bytes=0-99,500-1499,4000-

The first 100 bytes, 1000 bytes starting from the byte number 500,
and the last 200 bytes of the document:
Range: bytes=0-99,500-1499,-200

So to stay RFC compliant we also have to support the following formats:

  • Range: bytes=500- all bytes from byte 500 to the end of the file
  • Range: bytes=-500 the last 500 bytes of the file
  • Range: bytes=50-99,200-249 bytes 50-99 and 200-249

And all of that combined as well, e.g.

  • Range: bytes=0-99,500-1499,4000- bytes 0-99, 500-1499, 4000 to the end of the file
  • Range: bytes=0-99,500-1499,-200 bytes 0-99, 500-1499 and the last 200 bytes of the file

The easiest way is a foreach loop.

$fp = fopen('video.mp4', 'r');
$filesize = filesize("video.mp4");
foreach (split(substr($_SERVER['HTTP_RANGE'], 0, 6), ',') as $range) {
  preg_match_all("/(\d*)-(\d*)/", $range, $parts);
  if ($parts[2][0] == "") $parts[2][0] = $filesize;
  if ($parts[1][0] == "") {
    $parts[1][0] = $filesize - $parts[2][0];
    $parts[2][0] = $filesize;
  }
  fseek($fp, $parts[1][0]);
  echo fgets($fp, $parts[2][0] - $parts[1][0]);
}

We then start reading the file from byte $parts[1][0] for a length of $parts[2][0] - $parts[1][0] bytes.

4. Notes

Keep in mind that we may also need to set a file header aka MIME type in the PHP file so that the browser knows which format it is. For an .mp4 file that would look like this, for example:

header("Content-Type: video/mp4");
header("Content-Length: " . filesize("video.mp4"));

You can find all MIME types on the selfHTML page.

NewerSPAM protection methods N° 3: Timing is once again everythingOlderSPAM protection methods N° 1: Honeypot