HTTP

Header

Content-Disposition Header

Learn how the Content-Disposition header controls whether content displays inline or downloads as an attachment. Set custom filenames for file downloads.

5 min read intermediate Try in Playground

Content-Disposition Header

TL;DR: Controls whether content displays inline in the browser or downloads as a file. Use attachment to force downloads with a suggested filename.

What is Content-Disposition?

The Content-Disposition header tells the browser whether content should be displayed inline (in the browser) or downloaded as a file. It’s like telling someone “read this here” versus “take this home with you.”

This header is crucial for file downloads, attachments, and controlling how browsers handle different types of content.

How Content-Disposition Works

Server sends file for download:

HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="report.pdf"
Content-Length: 524288

[PDF file contents]
```text

**Browser opens download dialog instead of displaying the PDF inline.**

**Server sends for inline display:**

```http
HTTP/1.1 200 OK
Content-Type: image/jpeg
Content-Disposition: inline; filename="photo.jpg"

[Image data]

Browser displays the image directly in the page.

Syntax

Content-Disposition: inline
Content-Disposition: attachment
Content-Disposition: attachment; filename="filename.ext"
Content-Disposition: attachment; filename*=UTF-8''filename.ext
```text

### Directives

- **inline** - Display content in the browser
- **attachment** - Prompt user to download
- **filename** - Suggested filename for download
- **filename\*** - Encoded filename for internationalization

## Common Examples

### Force Download

```http
Content-Disposition: attachment; filename="document.pdf"

Triggers download dialog with suggested filename.

Display Inline

Content-Disposition: inline
```text

Browser attempts to display content (if supported type).

### Download with Special Characters

```http
Content-Disposition: attachment; filename*=UTF-8''%E6%96%87%E4%BB%B6.pdf

Handles non-ASCII characters in filename (文件.pdf).

Both ASCII and Encoded Filenames

Content-Disposition: attachment;
  filename="file.pdf";
  filename*=UTF-8''%E6%96%87%E4%BB%B6.pdf
```text

Fallback for older browsers that don't support filename\*.

## Real-World Scenarios

### Document Download Service

```http
GET /api/invoices/12345/download HTTP/1.1
Host: billing.example.com

HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="invoice-12345.pdf"
Content-Length: 245678

[PDF content]
GET /images/photo.jpg HTTP/1.1
Host: gallery.example.com

HTTP/1.1 200 OK
Content-Type: image/jpeg
Content-Disposition: inline
Cache-Control: public, max-age=31536000

[Image data]
```text

### CSV Export

```http
GET /api/reports/sales?format=csv HTTP/1.1
Host: analytics.example.com

HTTP/1.1 200 OK
Content-Type: text/csv; charset=utf-8
Content-Disposition: attachment; filename="sales-report-2026-01.csv"

Month,Sales,Revenue
January,150,$45000

Backup Download

GET /backup/database HTTP/1.1
Host: admin.example.com

HTTP/1.1 200 OK
Content-Type: application/gzip
Content-Disposition: attachment; filename="database-backup-2026-01-18.sql.gz"
Content-Length: 10485760

[Compressed backup data]
```javascript

## Server Implementation

### Express.js (Node.js)

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

// Force download
app.get('/download/:filename', (req, res) => {
  const filename = req.params.filename
  const filepath = path.join(__dirname, 'files', filename)

  res.download(filepath, filename, (err) => {
    if (err) {
      res.status(404).send('File not found')
    }
  })
})

// Manual implementation
app.get('/files/:id', (req, res) => {
  const filename = 'report.pdf'

  res.setHeader('Content-Type', 'application/pdf')
  res.setHeader('Content-Disposition', `attachment; filename="${filename}"`)

  // Send file
  fs.createReadStream(filepath).pipe(res)
})

// Inline display
app.get('/view/:filename', (req, res) => {
  res.setHeader('Content-Disposition', 'inline')
  res.sendFile(filepath)
})

// With UTF-8 filename
app.get('/download-intl/:filename', (req, res) => {
  const filename = '文档.pdf'
  const encodedFilename = encodeURIComponent(filename)

  res.setHeader(
    'Content-Disposition',
    `attachment; filename="${filename}"; filename*=UTF-8''${encodedFilename}`
  )

  res.sendFile(filepath)
})

Express with Dynamic Filename

app.get('/export/users', async (req, res) => {
  const users = await getUsersFromDatabase()
  const csv = convertToCSV(users)

  const timestamp = new Date().toISOString().split('T')[0]
  const filename = `users-export-${timestamp}.csv`

  res.setHeader('Content-Type', 'text/csv')
  res.setHeader('Content-Disposition', `attachment; filename="${filename}"`)

  res.send(csv)
})
```javascript

### FastAPI (Python)

```python
from fastapi import FastAPI
from fastapi.responses import FileResponse, StreamingResponse
import io

app = FastAPI()

@app.get("/download/{filename}")
async def download_file(filename: str):
    file_path = f"./files/{filename}"
    return FileResponse(
        file_path,
        media_type="application/octet-stream",
        filename=filename
    )

@app.get("/export/csv")
async def export_csv():
    csv_data = "Name,Email\nJohn,john@example.com\n"

    return StreamingResponse(
        io.StringIO(csv_data),
        media_type="text/csv",
        headers={
            "Content-Disposition": "attachment; filename=export.csv"
        }
    )

@app.get("/view/image/{image_id}")
async def view_image(image_id: str):
    return FileResponse(
        f"./images/{image_id}.jpg",
        media_type="image/jpeg",
        headers={"Content-Disposition": "inline"}
    )

Django

from django.http import FileResponse, HttpResponse
import urllib.parse

def download_file(request, file_id):
    file_path = f'/path/to/files/{file_id}.pdf'
    filename = 'document.pdf'

    response = FileResponse(open(file_path, 'rb'))
    response['Content-Type'] = 'application/pdf'
    response['Content-Disposition'] = f'attachment; filename="{filename}"'

    return response

def export_csv(request):
    data = get_data_from_database()
    csv_content = generate_csv(data)

    response = HttpResponse(csv_content, content_type='text/csv')
    response['Content-Disposition'] = 'attachment; filename="export.csv"'

    return response

def download_intl(request):
    filename = '文档.pdf'
    encoded = urllib.parse.quote(filename)

    response = FileResponse(open(file_path, 'rb'))
    response['Content-Disposition'] = \
        f'attachment; filename="{filename}"; filename*=UTF-8\'\'{encoded}'

    return response
```nginx

### Nginx

```nginx
server {
    listen 80;
    server_name files.example.com;

    # Force download for PDFs
    location /downloads/ {
        root /var/www;
        add_header Content-Disposition "attachment";
    }

    # Inline display for images
    location /images/ {
        root /var/www;
        add_header Content-Disposition "inline";
    }

    # Dynamic filename based on path
    location ~ ^/exports/(.+)\.csv$ {
        root /var/www/exports;
        add_header Content-Disposition "attachment; filename=$1.csv";
    }
}

Best Practices

For Servers

1. Always set Content-Type with Content-Disposition

# ✅ Both headers together
Content-Type: application/pdf
Content-Disposition: attachment; filename="doc.pdf"

# ❌ Missing Content-Type
Content-Disposition: attachment; filename="doc.pdf"
```javascript

**2. Use safe filenames**

```javascript
// ✅ Sanitize filename
function sanitizeFilename(filename) {
  return filename.replace(/[^a-zA-Z0-9.-]/g, '_').substring(0, 255)
}

const safeFilename = sanitizeFilename(userProvidedName)
res.setHeader('Content-Disposition', `attachment; filename="${safeFilename}"`)

// ❌ Using unsanitized user input
res.setHeader('Content-Disposition', `attachment; filename="${req.query.name}"`)

3. Support international filenames

function getContentDisposition(filename) {
  const ascii = filename.replace(/[^\x00-\x7F]/g, '_')
  const encoded = encodeURIComponent(filename)

  return `attachment; filename="${ascii}"; filename*=UTF-8''${encoded}`
}
```text

**4. Quote filenames with spaces**

```http
# ✅ Quoted filename
Content-Disposition: attachment; filename="my document.pdf"

# ❌ Unquoted (will break)
Content-Disposition: attachment; filename=my document.pdf

5. Set appropriate inline vs attachment

// Downloadable files
if (ext === '.pdf' || ext === '.doc' || ext === '.zip') {
  disposition = 'attachment'
}
// Viewable in browser
else if (ext === '.jpg' || ext === '.png' || ext === '.txt') {
  disposition = 'inline'
}
```javascript

### For Clients

**1. Handle both inline and attachment**

```javascript
fetch('/api/file/123').then((response) => {
  const disposition = response.headers.get('Content-Disposition')

  if (disposition && disposition.includes('attachment')) {
    // Download file
    return response.blob().then((blob) => {
      const url = URL.createObjectURL(blob)
      const a = document.createElement('a')
      a.href = url
      a.download = getFilenameFromHeader(disposition) || 'download'
      a.click()
    })
  } else {
    // Display inline
    return response.blob().then((blob) => {
      const url = URL.createObjectURL(blob)
      window.open(url)
    })
  }
})

2. Parse filename from header

function getFilenameFromHeader(disposition) {
  // Try filename* first (RFC 5987)
  const filenameStarMatch = disposition.match(/filename\*=UTF-8''(.+?)(?:;|$)/)
  if (filenameStarMatch) {
    return decodeURIComponent(filenameStarMatch[1])
  }

  // Fall back to filename
  const filenameMatch = disposition.match(/filename="(.+?)"/)
  if (filenameMatch) {
    return filenameMatch[1]
  }

  return 'download'
}
```text

## Filename Encoding

### ASCII Filenames

```http
Content-Disposition: attachment; filename="simple.pdf"

International Filenames (RFC 5987)

Content-Disposition: attachment; filename*=UTF-8''%E6%96%87%E6%A1%A3.pdf
```text

### Both for Maximum Compatibility

```http
Content-Disposition: attachment;
  filename="document.pdf";
  filename*=UTF-8''%E6%96%87%E6%A1%A3.pdf

Example Encoding Function

function encodeFilename(filename) {
  // ASCII fallback
  const asciiFilename = filename.replace(/[^\x00-\x7F]/g, '_')

  // UTF-8 encoded
  const utf8Filename = encodeURIComponent(filename)

  return `attachment; filename="${asciiFilename}"; filename*=UTF-8''${utf8Filename}`
}

// Usage
res.setHeader('Content-Disposition', encodeFilename('文档.pdf'))
```text

## Form Data Usage

Content-Disposition is also used in multipart form data:

```http
POST /upload HTTP/1.1
Host: api.example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="photo.jpg"
Content-Type: image/jpeg

[file data]
------WebKitFormBoundary--

Testing Content-Disposition

Using curl

# Check Content-Disposition header
curl -I https://api.example.com/download/file.pdf

# Download and save with suggested filename
curl -OJ https://api.example.com/download/file.pdf

# View header value
curl -s -D - https://api.example.com/file -o /dev/null | grep Content-Disposition
```javascript

### Using JavaScript

```javascript
// Download file with proper filename
async function downloadFile(url) {
  const response = await fetch(url)
  const blob = await response.blob()

  const disposition = response.headers.get('Content-Disposition')
  const filename = getFilenameFromHeader(disposition) || 'download'

  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = filename
  document.body.appendChild(a)
  a.click()
  document.body.removeChild(a)
  URL.revokeObjectURL(url)
}

downloadFile('/api/export/report')

Browser Testing

// Test inline vs attachment
const testUrl = 'https://api.example.com/file'

fetch(testUrl).then((res) => {
  console.log('Content-Disposition:', res.headers.get('Content-Disposition'))
  console.log('Content-Type:', res.headers.get('Content-Type'))
})

Frequently Asked Questions

What is Content-Disposition?

Content-Disposition tells browsers how to handle the response. "inline" displays in browser, "attachment" triggers download. You can also specify a filename for downloads.

How do I force a file download?

Use Content-Disposition: attachment; filename="file.pdf". The attachment value triggers download instead of display, and filename suggests the save name.

How do I handle special characters in filenames?

Use filename* with UTF-8 encoding: filename*=UTF-8 double-quote-percent-encoded-name. For compatibility, include both filename and filename* parameters.

What is the difference between inline and attachment?

inline displays content in the browser if possible (PDFs, images). attachment always triggers the download dialog. Use inline for preview, attachment for download.

Keep Learning