HTTP

Header

If-Range Header

Learn how the If-Range header requests partial content only if the resource is unchanged. Efficiently resume downloads without re-fetching entire files.

7 min read advanced Try in Playground

What is If-Range?

TL;DR: Makes range requests conditional on resource version. If the ETag/date matches, returns the requested range; if changed, returns the full new resource.

The If-Range header makes range requests conditional based on whether the resource has changed. It’s like saying “send me bytes 1000-2000, but only if this is still the same file; otherwise send me the whole thing.”

This is useful for resuming downloads when you want to ensure you’re resuming the same version of the file.

How If-Range Works

Initial download (interrupted):

GET /video.mp4 HTTP/1.1
Host: cdn.example.com

HTTP/1.1 200 OK
ETag: "abc123"
Accept-Ranges: bytes
Content-Length: 104857600

[Download interrupted at 50MB]
```http

**Resume with If-Range:**

```http
GET /video.mp4 HTTP/1.1
Host: cdn.example.com
Range: bytes=52428800-
If-Range: "abc123"

If ETag matches (file unchanged):

HTTP/1.1 206 Partial Content
Content-Range: bytes 52428800-104857599/104857600
ETag: "abc123"

[Remaining 50MB]
```text

**If ETag doesn't match (file changed):**

```http
HTTP/1.1 200 OK
ETag: "def456"
Content-Length: 104857600

[Complete new file]

Syntax

If-Range: "<etag>"
If-Range: <http-date>
```text

### Values

- **"etag"** - ETag value (must be quoted, must be strong ETag)
- **http-date** - Last-Modified date

## Common Examples

### Resume with ETag

```http
Range: bytes=10485760-
If-Range: "abc123"

Resume download only if ETag matches.

Resume with Date

Range: bytes=10485760-
If-Range: Wed, 15 Jan 2026 10:00:00 GMT
```text

Resume only if file hasn't been modified since date.

### Video Streaming

```http
Range: bytes=52428800-
If-Range: "video-v5"

Continue streaming from specific point if video hasn’t changed.

Real-World Scenarios

Download Manager Resume

# Initial download
GET /files/ubuntu.iso HTTP/1.1
Host: downloads.example.com

HTTP/1.1 200 OK
ETag: "iso-version-22.04"
Last-Modified: Mon, 13 Jan 2026 08:00:00 GMT
Accept-Ranges: bytes
Content-Length: 3221225472

[Download starts... gets to 50%... connection drops]

# Resume attempt
GET /files/ubuntu.iso HTTP/1.1
Range: bytes=1610612736-
If-Range: "iso-version-22.04"

# File unchanged - partial content
HTTP/1.1 206 Partial Content
Content-Range: bytes 1610612736-3221225471/3221225472
ETag: "iso-version-22.04"

[Remaining 50%]
```text

### File Changed During Download

```http
# Try to resume, but file was updated
GET /files/ubuntu.iso HTTP/1.1
Range: bytes=1610612736-
If-Range: "iso-version-22.04"

# File changed - send full new version
HTTP/1.1 200 OK
ETag: "iso-version-24.04"
Content-Length: 3400000000

[Complete new file]

Video Player Seeking

# User seeks to 5 minutes in
GET /videos/movie.mp4 HTTP/1.1
Range: bytes=52428800-53477375
If-Range: "movie-1080p-v3"

# Video hasn't changed - send requested chunk
HTTP/1.1 206 Partial Content
Content-Range: bytes 52428800-53477375/524288000
ETag: "movie-1080p-v3"

[1MB chunk at 5 minute mark]
```text

### PDF Viewer Progressive Load

```http
# Load page 10 of document
GET /documents/manual.pdf HTTP/1.1
Range: bytes=1048576-1153023
If-Range: Sat, 18 Jan 2026 09:00:00 GMT

# Document unchanged since that time
HTTP/1.1 206 Partial Content
Content-Range: bytes 1048576-1153023/5242880

[Page 10 data]

Server Implementation

Express.js (Node.js)

const express = require('express')
const fs = require('fs')
const crypto = require('crypto')
const app = express()

function generateETag(filePath) {
  const content = fs.readFileSync(filePath)
  return `"${crypto.createHash('md5').update(content).digest('hex')}"`
}

app.get('/download/:filename', (req, res) => {
  const filePath = `./files/${req.params.filename}`

  if (!fs.existsSync(filePath)) {
    return res.status(404).send('File not found')
  }

  const stat = fs.statSync(filePath)
  const fileSize = stat.size
  const lastModified = stat.mtime.toUTCString()
  const etag = generateETag(filePath)

  const range = req.headers.range
  const ifRange = req.headers['if-range']

  // No range requested
  if (!range) {
    res.setHeader('Accept-Ranges', 'bytes')
    res.setHeader('ETag', etag)
    res.setHeader('Last-Modified', lastModified)
    return res.sendFile(filePath)
  }

  // Range requested with If-Range
  if (ifRange) {
    let matches = false

    // Check ETag match
    if (ifRange.startsWith('"')) {
      matches = ifRange === etag
    }
    // Check date match
    else {
      const ifRangeDate = new Date(ifRange)
      const fileDate = new Date(lastModified)
      matches = fileDate <= ifRangeDate
    }

    // If-Range doesn't match - send full file
    if (!matches) {
      res.setHeader('ETag', etag)
      res.setHeader('Last-Modified', lastModified)
      res.setHeader('Accept-Ranges', 'bytes')
      return res.sendFile(filePath)
    }
  }

  // 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(filePath, { start, end })

  res.status(206)
  res.setHeader('Content-Range', `bytes ${start}-${end}/${fileSize}`)
  res.setHeader('Content-Length', chunkSize)
  res.setHeader('Accept-Ranges', 'bytes')
  res.setHeader('ETag', etag)
  res.setHeader('Last-Modified', lastModified)

  file.pipe(res)
})
```javascript

### FastAPI (Python)

```python
from fastapi import FastAPI, Request, Response
from fastapi.responses import StreamingResponse, FileResponse
import os
import hashlib
from datetime import datetime
from email.utils import formatdate, parsedate_to_datetime

app = FastAPI()

def generate_etag(file_path):
    with open(file_path, 'rb') as f:
        content = f.read()
        return f'"{hashlib.md5(content).hexdigest()}"'

@app.get("/download/{filename}")
async def download_file(filename: str, request: Request, response: Response):
    file_path = f"./files/{filename}"

    if not os.path.exists(file_path):
        return Response(status_code=404, content="File not found")

    file_size = os.path.getsize(file_path)
    file_mtime = os.path.getmtime(file_path)
    last_modified = formatdate(timeval=file_mtime, localtime=False, usegmt=True)
    etag = generate_etag(file_path)

    range_header = request.headers.get('range')
    if_range = request.headers.get('if-range')

    # No range requested
    if not range_header:
        return FileResponse(
            file_path,
            headers={
                'Accept-Ranges': 'bytes',
                'ETag': etag,
                'Last-Modified': last_modified
            }
        )

    # Check If-Range condition
    if if_range:
        matches = False

        # Check ETag match
        if if_range.startswith('"'):
            matches = if_range == etag
        # Check date match
        else:
            try:
                if_range_date = parsedate_to_datetime(if_range)
                file_date = datetime.fromtimestamp(file_mtime)
                matches = file_date <= if_range_date.replace(tzinfo=None)
            except:
                matches = False

        # If-Range doesn't match - send full file
        if not matches:
            return FileResponse(
                file_path,
                headers={
                    'ETag': etag,
                    'Last-Modified': last_modified,
                    'Accept-Ranges': 'bytes'
                }
            )

    # Parse range
    import re
    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(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

    return StreamingResponse(
        iterfile(),
        status_code=206,
        headers={
            'Content-Range': f'bytes {start}-{end}/{file_size}',
            'Content-Length': str(chunk_size),
            'Accept-Ranges': 'bytes',
            'ETag': etag,
            'Last-Modified': last_modified
        }
    )

Best Practices

For Servers

1. Support both ETag and Last-Modified

// ✅ Check both validators
if (ifRange) {
  if (ifRange.startsWith('"')) {
    // ETag comparison
    matches = ifRange === currentETag
  } else {
    // Date comparison
    matches = new Date(lastModified) <= new Date(ifRange)
  }
}
```text

**2. Send full file if If-Range doesn't match**

```http
# ✅ If-Range fails - return 200 with full file
HTTP/1.1 200 OK
Content-Length: 104857600

[Full file]

# ❌ Don't return 412 or error

3. Only use strong ETags

// ✅ Strong ETag for range requests
const etag = `"${hash(file)}"`

// ❌ Weak ETags don't work with If-Range
const etag = `W/"${hash(file)}"`
```http

**4. Include validators in partial responses**

```http
HTTP/1.1 206 Partial Content
Content-Range: bytes 1000-2000/10000
ETag: "abc123"
Last-Modified: Sat, 18 Jan 2026 10:00:00 GMT

5. Handle missing If-Range gracefully

// If Range but no If-Range, just send the range
if (range && !ifRange) {
  // Send partial content without checking
}
```javascript

### For Clients

**1. Always use If-Range when resuming**

```javascript
// ✅ Safe resume with If-Range
const etag = previousResponse.headers.get('ETag')

await fetch('/file.zip', {
  headers: {
    Range: 'bytes=1000000-',
    'If-Range': etag
  }
})

2. Handle both 200 and 206 responses

const response = await fetch(url, {
  headers: {
    Range: `bytes=${bytesDownloaded}-`,
    'If-Range': storedETag
  }
})

if (response.status === 206) {
  // Resume successful - append data
  appendToFile(await response.blob())
} else if (response.status === 200) {
  // File changed - restart download
  restartDownload(await response.blob())
}
```javascript

**3. Store validators with partial downloads**

```javascript
// ✅ Store ETag/Last-Modified for resume
const downloadState = {
  bytesDownloaded: 1000000,
  etag: response.headers.get('ETag'),
  lastModified: response.headers.get('Last-Modified'),
  totalSize: 10000000
}

localStorage.setItem('download', JSON.stringify(downloadState))

4. Use ETag over Last-Modified when available

// ✅ Prefer ETag (more reliable)
const validator = response.headers.get('ETag') || response.headers.get('Last-Modified')

fetch(url, {
  headers: {
    Range: 'bytes=1000-',
    'If-Range': validator
  }
})
```text

## If-Range vs If-Match

### If-Range (Resume Downloads)

```http
# "Send range if file is same, otherwise send full file"
Range: bytes=1000-
If-Range: "abc123"

# 206 Partial Content = Same file, here's your range
# 200 OK = File changed, here's the new complete file

If-Match (Conditional Updates)

# "Only update if file is still this version"
PUT /file
If-Match: "abc123"

# 200 OK = Updated successfully
# 412 Precondition Failed = File changed, update rejected
```text

## Common Use Cases

### Download Resume

```http
Range: bytes=50000000-
If-Range: "file-v5"

Resume large file download safely.

Video Streaming

Range: bytes=10485760-
If-Range: "video-1080p"
```text

Continue video from specific point.

### Progressive PDF Loading

```http
Range: bytes=102400-204799
If-Range: Sat, 18 Jan 2026 09:00:00 GMT

Load PDF pages progressively.

Audio Streaming

Range: bytes=2097152-
If-Range: "audio-track-3"
```text

Resume audio from specific position.

## Testing If-Range

### Using curl

```bash
# Initial download (interrupt with Ctrl+C)
curl -o file.zip https://example.com/file.zip

# Get ETag
ETAG=$(curl -sI https://example.com/file.zip | grep -i etag | cut -d' ' -f2)

# Resume with If-Range
curl -H "Range: bytes=1000000-" \
     -H "If-Range: $ETAG" \
     https://example.com/file.zip

# Test with wrong ETag (should get full file)
curl -H "Range: bytes=1000000-" \
     -H "If-Range: \"wrong-etag\"" \
     https://example.com/file.zip

Using JavaScript

// Download with resume support
class ResumableDownload {
  constructor(url) {
    this.url = url
    this.bytesDownloaded = 0
    this.etag = null
    this.chunks = []
  }

  async download() {
    const headers = {}

    if (this.bytesDownloaded > 0 && this.etag) {
      headers['Range'] = `bytes=${this.bytesDownloaded}-`
      headers['If-Range'] = this.etag
    }

    const response = await fetch(this.url, { headers })

    if (response.status === 206) {
      // Resume successful
      console.log('Resuming from', this.bytesDownloaded)
    } else if (response.status === 200) {
      // File changed or no range support - restart
      console.log('Starting fresh download')
      this.bytesDownloaded = 0
      this.chunks = []
    }

    // Store ETag for next resume
    this.etag = response.headers.get('ETag')

    // Read data
    const blob = await response.blob()
    this.chunks.push(blob)
    this.bytesDownloaded += blob.size

    return blob
  }

  getFile() {
    return new Blob(this.chunks)
  }
}

// Usage
const download = new ResumableDownload('/large-file.zip')
await download.download()
  • Range - Request specific byte range
  • Content-Range - Server indicates which bytes are sent
  • ETag - Resource version identifier
  • If-Match - Conditional update based on ETag

Frequently Asked Questions

What is If-Range?

If-Range makes range requests conditional. If the resource has not changed (ETag or date matches), the server returns the requested range. If changed, it returns the full new resource.

When should I use If-Range?

Use If-Range when resuming downloads. It ensures you get the remaining bytes if unchanged, or the complete new file if it changed, avoiding corrupted partial downloads.

What values can If-Range contain?

If-Range can contain either an ETag or a Last-Modified date. ETags are preferred for precision. The value should match what the server sent with the original partial response.

What is the difference between If-Range and If-Match?

If-Range is for range requests and returns full content on mismatch. If-Match is for updates and returns 412 error on mismatch. If-Range is more forgiving.

Keep Learning