HTTP Range Header

Short explanation of the HTTP range header.

1204 views

Edited: 2020-07-04 14:55

The HTTP range request header may be used to request specific parts of a resource.

A server may specify to clients that the range header is supported by adding the accept-ranges to the response headers, this is done like this:

HTTP/1.1 200 Ok
accept-ranges: bytes

The range header is used when streaming audio and video files, in order to enable "skipping" with the controls. But, it is also used to allow clients to pause and resume downloads.

Below is a simple way to extract a single range from the range header:

// Define start and end of stream
$start = 0;
$end = $file_size - 1; // Minus 1 (Byte ranges are zero-indexed)

if (preg_match('|=([0-9]+)-([0-9]+)$|', $_SERVER['HTTP_RANGE'], $matches)) {
  $start = $matches["1"];
  $end = $matches["2"] - 1;
} elseif (preg_match('|=([0-9]+)-?$|', $_SERVER['HTTP_RANGE'], $matches)) {
  // If no end-range was provided
  $start = $matches["1"];
}
  
// Make sure we are not out of range
if (($start > $end) || ($start > $file_size) || ($end > $file_size) || ($end <= $start)) {
  http_response_code(416);
  exit();
}

This is not enough to support the range header, but it shows you how it can be handled using regular expressions.

Supporting the range header

Your web server might already support the range header for static files, but if you want to serve files from PHP, you will need to implement your own range header support.

The function below will allow you to "stream" static files from within PHP:

function http_stream_file($file_path) {
    if (!file_exists($file_path)) {
      throw new Exception('The file did not exist');
    }
    if (($file_size = filesize($file_path)) === false) {
      throw new Exception('Unable to get filesize.');
    }
  
  // Define start and end of stream
  $start = 0;
  $end = $file_size - 1; // Minus 1 (Byte ranges are zero-indexed)
  
  // Attempt to Open file for (r) reading (b=binary safe)
  if (($fp = @fopen($file_path, 'rb')) == false) {
    throw new Exception('Unable to open file.');
  }
  
  
  // -----------------------
  // Handle "range" requests
  // -----------------------
  // A Range request is sent when a client requests a specific part of a file
  // such as when using the video controls or when a download is resumed.
  // We need to handle range requests in order to send back the requested part of a file.
  
  // Determine if the "range" Request Header was set
  if (isset($_SERVER['HTTP_RANGE'])) {
  
    // Parse the range header
    if (preg_match('|=([0-9]+)-([0-9]+)$|', $_SERVER['HTTP_RANGE'], $matches)) {
      $start = $matches["1"];
      $end = $matches["2"] - 1;
    } elseif (preg_match('|=([0-9]+)-?$|', $_SERVER['HTTP_RANGE'], $matches)) {
      $start = $matches["1"];
    }
  
    // Make sure we are not out of range
    if (($start > $end) || ($start > $file_size) || ($end > $file_size) || ($end <= $start)) {
      http_response_code(416);
      exit();
    }
  
    // Position the file pointer at the requested range
    fseek($fp, $start);
  
    // Respond with 206 Partial Content
    http_response_code(206);
  
    // A "content-range" response header should only be sent if the "range" header was used in the request
      $response_headers['content-range'] = 'bytes ' . $start . '-' . $end . '/' . $file_size;
  } else {
    // If the range header is not used, respond with a 200 code and start sending some content
    http_response_code(200);
  }
  
  // Tell the client we support range-requests
  $response_headers['accept-ranges'] = 'bytes';
  // Set the content length to whatever remains
  $response_headers['content-length'] = ($file_size - $start);
  
  // ---------------------
  // Send the file headers
  // ---------------------
  // Send the "last-modified" response header
  // and compare with the "if-modified-since" request header (if present)
  
  if (($timestamp = filemtime($file_path)) !== false) {
    $response_headers['last-modified'] = gmdate("D, d M Y H:i:s", $timestamp) . ' GMT';
    if ((isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) && ($_SERVER['HTTP_IF_MODIFIED_SINCE'] == $response_headers['last-modified'])) {
      http_response_code(304); // Not Modified
      exit();
    }
  }
  
  // Set HTTP response headers
  $response_headers['content-type'] = 'video/mp4';

  foreach ($response_headers as $header => $value) {
    header($header . ': ' . $value);
  }
  
  // ---------------------
  // Start the file output
  // ---------------------
  $buffer = 8192;
  while (!feof($fp) && ($pointer = ftell($fp)) <= $end) {
  
    // If next $buffer will pass $end,
    // calculate remaining size
    if ($pointer + $buffer > $end) {
      $buffer = $end - $pointer + 1;
    }
    echo @fread($fp, $buffer);
      flush();
    }
    fclose($fp);
    exit();
  }

To call this function on a file that resides in the same directory, we can do like this:

http_stream_file('some-video-file.mp4');

See also: Streaming mp4 files from PHP

Tell us what you think:

  1. An in-dept look at the use of headings (h1-h6) and sections in HTML pages.
  2. Pagination can be a confusing thing to get right both practically and programmatically. I have put a lot of thought into this subject, and here I am giving you a few of the ideas I have been working with.
  3. The best way to deal with a trailing question mark is probably just to make it a bad request, because it is a very odd thing to find in a request URL.
  4. How to optimize image-loading and automatically include width and height attributes on img elements with PHP.
  5. HTTP headers are not case-sensitive, so we are free to convert them to all-lowercase in our applications.

More in: Web development