- Home
- HTTP Headers
- Content-Range Header
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.
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)
})
Related Headers
- Range - Client specifies desired byte range
- Accept-Ranges - Server indicates range support
- Content-Length - Size of the partial content
- If-Range - Conditional range request based on ETag
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.