HTTP

Header

Content-Range Header

Learn how the Content-Range header indicates which portion of a resource is being sent in partial content (206) responses for range requests and streaming.

7 min read intermediate Try in Playground

Content-Range Header

TL;DR: Indicates which portion of a resource is being sent in a 206 Partial Content response. Essential for range requests, video streaming, and resumable downloads.

What is Content-Range?

The Content-Range header tells the client which portion of a resource is being sent in response to a range request. It’s like saying “Here are pages 10-20 of a 100-page book” or “Here’s chunk 2 of 10.”

This header is essential for partial downloads, video streaming, large file transfers, and resume functionality.

How Content-Range Works

Client requests specific byte range:

GET /video.mp4 HTTP/1.1
Host: cdn.example.com
Range: bytes=0-1048575
```http

**Server responds with partial content:**

```http
HTTP/1.1 206 Partial Content
Content-Type: video/mp4
Content-Range: bytes 0-1048575/104857600
Content-Length: 1048576
Accept-Ranges: bytes

[First 1MB of video]

Syntax

Content-Range: <unit> <range-start>-<range-end>/<total-size>
Content-Range: <unit> <range-start>-<range-end>/*
Content-Range: <unit> */<total-size>
```text

### Values

- **unit** - Range unit (always `bytes`)
- **range-start** - Starting byte position (0-indexed)
- **range-end** - Ending byte position (inclusive)
- **total-size** - Total size of the resource in bytes
- **\*** - Unknown size

## Common Examples

### First Chunk of File

```http
Content-Range: bytes 0-1023/10240

Sending first 1KB of a 10KB file.

Middle Chunk

Content-Range: bytes 5000-9999/50000
```text

Sending bytes 5000-9999 of a 50KB file.

### Resume Download

```http
Content-Range: bytes 10485760-20971519/104857600

Resuming from 10MB, sending up to 20MB of a 100MB file.

Unknown Total Size

Content-Range: bytes 0-1023/*
```text

Sending first 1KB, total size unknown (streaming).

## Real-World Scenarios

### Video Streaming

```http
# Initial request
GET /videos/movie.mp4 HTTP/1.1
Host: streaming.example.com
Range: bytes=0-1048575

# Server response
HTTP/1.1 206 Partial Content
Content-Type: video/mp4
Content-Range: bytes 0-1048575/524288000
Content-Length: 1048576
Accept-Ranges: bytes

[First 1MB of video]

# User seeks to 5 minutes (approx 50MB in)
GET /videos/movie.mp4 HTTP/1.1
Range: bytes=52428800-53477375

# Server response
HTTP/1.1 206 Partial Content
Content-Range: bytes 52428800-53477375/524288000
Content-Length: 1048576

[Chunk at 5 minute mark]

Download Manager Resume

# Initial download interrupted at 50%
GET /files/large-file.zip HTTP/1.1
Host: downloads.example.com
Range: bytes=52428800-

# Server response
HTTP/1.1 206 Partial Content
Content-Type: application/zip
Content-Range: bytes 52428800-104857599/104857600
Content-Length: 52428800
Accept-Ranges: bytes

[Remaining 50MB]
```text

### PDF Viewer Progressive Loading

```http
# Load first page
GET /documents/manual.pdf HTTP/1.1
Range: bytes=0-102399

HTTP/1.1 206 Partial Content
Content-Type: application/pdf
Content-Range: bytes 0-102399/5242880
Content-Length: 102400

[First 100KB]

# Load page 5
GET /documents/manual.pdf HTTP/1.1
Range: bytes=409600-511999

HTTP/1.1 206 Partial Content
Content-Range: bytes 409600-511999/5242880
Content-Length: 102400

[Page 5 data]

Audio Streaming

GET /audio/song.mp3 HTTP/1.1
Range: bytes=1048576-2097151

HTTP/1.1 206 Partial Content
Content-Type: audio/mpeg
Content-Range: bytes 1048576-2097151/10485760
Accept-Ranges: bytes

[1MB chunk of audio]
```javascript

## Server Implementation

### Express.js (Node.js)

```javascript
const express = require('express')
const fs = require('fs')
const app = express()

app.get('/video/:id', (req, res) => {
  const videoPath = `./videos/${req.params.id}.mp4`
  const stat = fs.statSync(videoPath)
  const fileSize = stat.size
  const range = req.headers.range

  if (!range) {
    // No range requested, send entire file
    res.setHeader('Content-Length', fileSize)
    res.setHeader('Content-Type', 'video/mp4')
    res.setHeader('Accept-Ranges', 'bytes')
    fs.createReadStream(videoPath).pipe(res)
    return
  }

  // Parse range
  const parts = range.replace(/bytes=/, '').split('-')
  const start = parseInt(parts[0], 10)
  const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1

  // Validate range
  if (start >= fileSize || end >= fileSize || start > end) {
    res.status(416)
    res.setHeader('Content-Range', `bytes */${fileSize}`)
    return res.end()
  }

  const chunkSize = end - start + 1
  const file = fs.createReadStream(videoPath, { start, end })

  res.status(206)
  res.setHeader('Content-Range', `bytes ${start}-${end}/${fileSize}`)
  res.setHeader('Content-Length', chunkSize)
  res.setHeader('Content-Type', 'video/mp4')
  res.setHeader('Accept-Ranges', 'bytes')

  file.pipe(res)
})

Multiple Range Handling

app.get('/download/:filename', (req, res) => {
  const filepath = `./files/${req.params.filename}`
  const stat = fs.statSync(filepath)
  const fileSize = stat.size
  const range = req.headers.range

  if (!range) {
    res.setHeader('Accept-Ranges', 'bytes')
    res.sendFile(filepath)
    return
  }

  // Support for multiple ranges
  const ranges = parseRangeHeader(range, fileSize)

  if (!ranges || ranges.length === 0) {
    res.status(416)
    res.setHeader('Content-Range', `bytes */${fileSize}`)
    return res.end()
  }

  if (ranges.length === 1) {
    // Single range
    const { start, end } = ranges[0]
    const chunkSize = end - start + 1

    res.status(206)
    res.setHeader('Content-Range', `bytes ${start}-${end}/${fileSize}`)
    res.setHeader('Content-Length', chunkSize)
    res.setHeader('Content-Type', 'application/octet-stream')

    fs.createReadStream(filepath, { start, end }).pipe(res)
  } else {
    // Multiple ranges - use multipart/byteranges
    const boundary = 'MULTIPART_BOUNDARY'

    res.status(206)
    res.setHeader('Content-Type', `multipart/byteranges; boundary=${boundary}`)

    // Send each range as separate part
    for (const { start, end } of ranges) {
      res.write(`--${boundary}\r\n`)
      res.write(`Content-Range: bytes ${start}-${end}/${fileSize}\r\n\r\n`)

      const chunk = fs.readFileSync(filepath, {
        start,
        end,
        encoding: null
      })
      res.write(chunk)
      res.write('\r\n')
    }

    res.write(`--${boundary}--\r\n`)
    res.end()
  }
})
```javascript

### FastAPI (Python)

```python
from fastapi import FastAPI, Request, Response, status
from fastapi.responses import StreamingResponse
import os
import re

app = FastAPI()

@app.get("/video/{video_id}")
async def stream_video(video_id: str, request: Request):
    video_path = f"./videos/{video_id}.mp4"
    file_size = os.path.getsize(video_path)

    range_header = request.headers.get('range')

    if not range_header:
        # Return full file
        def iterfile():
            with open(video_path, 'rb') as f:
                yield from f

        return StreamingResponse(
            iterfile(),
            media_type="video/mp4",
            headers={
                "Accept-Ranges": "bytes",
                "Content-Length": str(file_size)
            }
        )

    # Parse range
    match = re.match(r'bytes=(\d+)-(\d*)', range_header)
    if not match:
        return Response(status_code=416)

    start = int(match.group(1))
    end = int(match.group(2)) if match.group(2) else file_size - 1

    # Validate range
    if start >= file_size or end >= file_size or start > end:
        return Response(
            status_code=416,
            headers={"Content-Range": f"bytes */{file_size}"}
        )

    chunk_size = end - start + 1

    def iterfile():
        with open(video_path, 'rb') as f:
            f.seek(start)
            remaining = chunk_size
            while remaining:
                chunk = f.read(min(8192, remaining))
                if not chunk:
                    break
                remaining -= len(chunk)
                yield chunk

    return StreamingResponse(
        iterfile(),
        status_code=206,
        media_type="video/mp4",
        headers={
            "Content-Range": f"bytes {start}-{end}/{file_size}",
            "Content-Length": str(chunk_size),
            "Accept-Ranges": "bytes"
        }
    )

Django

from django.http import StreamingHttpResponse, HttpResponse
import os
import re

def stream_file(request, filename):
    file_path = f'./files/{filename}'
    file_size = os.path.getsize(file_path)

    range_header = request.META.get('HTTP_RANGE', '')

    if not range_header:
        response = StreamingHttpResponse(
            open(file_path, 'rb'),
            content_type='application/octet-stream'
        )
        response['Content-Length'] = file_size
        response['Accept-Ranges'] = 'bytes'
        return response

    # Parse range
    match = re.match(r'bytes=(\d+)-(\d*)', range_header)
    if not match:
        response = HttpResponse(status=416)
        response['Content-Range'] = f'bytes */{file_size}'
        return response

    start = int(match.group(1))
    end = int(match.group(2)) if match.group(2) else file_size - 1

    # Validate
    if start >= file_size or end >= file_size or start > end:
        response = HttpResponse(status=416)
        response['Content-Range'] = f'bytes */{file_size}'
        return response

    chunk_size = end - start + 1

    def file_iterator():
        with open(file_path, 'rb') as f:
            f.seek(start)
            remaining = chunk_size
            while remaining:
                chunk = f.read(min(8192, remaining))
                if not chunk:
                    break
                remaining -= len(chunk)
                yield chunk

    response = StreamingHttpResponse(
        file_iterator(),
        status=206,
        content_type='application/octet-stream'
    )
    response['Content-Range'] = f'bytes {start}-{end}/{file_size}'
    response['Content-Length'] = chunk_size
    response['Accept-Ranges'] = 'bytes'

    return response
```text

## Best Practices

### For Servers

**1. Always validate range requests**

```javascript
if (start >= fileSize || end >= fileSize || start > end) {
  res.status(416)
  res.setHeader('Content-Range', `bytes */${fileSize}`)
  return res.end()
}

2. Set all required headers

# ✅ Complete response
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-1023/10240
Content-Length: 1024
Content-Type: video/mp4
Accept-Ranges: bytes
```javascript

**3. Use ETag for conditional range requests**

```javascript
const etag = generateETag(file)

if (req.headers['if-range'] === etag) {
  // Send range
  res.status(206)
  res.setHeader('Content-Range', `bytes ${start}-${end}/${size}`)
  res.setHeader('ETag', etag)
} else {
  // ETag doesn't match, send full file
  res.status(200)
  res.setHeader('Content-Length', size)
}

4. Handle unsatisfiable ranges properly

HTTP/1.1 416 Range Not Satisfiable
Content-Range: bytes */10240
Content-Type: text/plain

Requested range not satisfiable
```text

**5. Support Accept-Ranges header**

```javascript
// Always indicate range support
res.setHeader('Accept-Ranges', 'bytes')

For Clients

1. Handle 206 and 200 responses

fetch('/video.mp4', {
  headers: { Range: 'bytes=0-1048575' }
}).then((response) => {
  if (response.status === 206) {
    const contentRange = response.headers.get('Content-Range')
    console.log('Partial content:', contentRange)
  } else if (response.status === 200) {
    console.log('Full content (range not supported)')
  }
})
```javascript

**2. Parse Content-Range header**

```javascript
function parseContentRange(header) {
  // "bytes 0-1023/10240"
  const match = header.match(/bytes (\d+)-(\d+)\/(\d+|\*)/)

  if (match) {
    return {
      start: parseInt(match[1]),
      end: parseInt(match[2]),
      total: match[3] === '*' ? null : parseInt(match[3])
    }
  }

  return null
}

3. Verify received range

const requestedStart = 1000
const requestedEnd = 2000

fetch('/file', {
  headers: { Range: `bytes=${requestedStart}-${requestedEnd}` }
}).then((response) => {
  const contentRange = parseContentRange(response.headers.get('Content-Range'))

  if (contentRange.start !== requestedStart || contentRange.end !== requestedEnd) {
    console.warn('Server sent different range than requested')
  }
})
```text

## Range Request Patterns

### Single Range

```http
Range: bytes=0-1023
Content-Range: bytes 0-1023/10240

Open-Ended Range

Range: bytes=5000-
Content-Range: bytes 5000-9999/10000
```http

### Suffix Range (Last N Bytes)

```http
Range: bytes=-1000
Content-Range: bytes 9000-9999/10000

Multiple Ranges

Range: bytes=0-1023, 5000-6000
Content-Type: multipart/byteranges; boundary=BOUNDARY
```text

## Testing Content-Range

### Using curl

```bash
# Request first 1KB
curl -H "Range: bytes=0-1023" https://example.com/video.mp4 -v

# Request from byte 1000 to end
curl -H "Range: bytes=1000-" https://example.com/file.zip -v

# Request last 1KB
curl -H "Range: bytes=-1024" https://example.com/file.pdf -v

# Check headers only
curl -H "Range: bytes=0-1023" -I https://example.com/video.mp4

Using JavaScript

// Request partial content
fetch('https://cdn.example.com/video.mp4', {
  headers: {
    Range: 'bytes=0-1048575'
  }
})
  .then((response) => {
    console.log('Status:', response.status) // Should be 206
    console.log('Content-Range:', response.headers.get('Content-Range'))
    console.log('Content-Length:', response.headers.get('Content-Length'))

    return response.arrayBuffer()
  })
  .then((buffer) => {
    console.log('Received bytes:', buffer.byteLength)
  })

Frequently Asked Questions

What is Content-Range?

Content-Range indicates which part of a resource is being sent in a 206 Partial Content response. Format: Content-Range: bytes 0-999/5000 means bytes 0-999 of 5000 total.

How do I read Content-Range values?

Format is "bytes start-end/total". bytes 200-999/5000 means bytes 200 through 999 are included, and the full resource is 5000 bytes. Use * for unknown total size.

When is Content-Range sent?

Servers send Content-Range with 206 Partial Content responses to Range requests. It tells clients exactly which bytes are in the response body.

What does Content-Range: bytes */5000 mean?

This unsatisfied range response (with 416 status) means the requested range was invalid. The total size is 5000 bytes but the requested range could not be satisfied.

Keep Learning