Skip to main content

Express Content Disposition

When building web applications with Express.js, you'll often need to control how the browser handles files and content you're sending. The Content-Disposition header is a powerful tool that tells browsers whether to display content directly in the browser window or to treat it as a downloadable file. This guide will walk you through using Content-Disposition headers in Express applications.

What is Content-Disposition?

The Content-Disposition HTTP header indicates if content should be displayed inline (directly in the browser window) or as an attachment (prompting the user to download it). Additionally, it can specify the filename for the downloaded content.

There are two main values for Content-Disposition:

  • inline: Display the content in the browser if possible
  • attachment: Prompt the user to download the content

Basic Usage in Express

Let's start with a simple example of how to set the Content-Disposition header in Express:

javascript
const express = require('express');
const app = express();

app.get('/document', (req, res) => {
// Set Content-Disposition header to make the file downloadable
res.setHeader('Content-Disposition', 'attachment; filename="document.pdf"');

// Set the appropriate content type
res.setHeader('Content-Type', 'application/pdf');

// Send the file data
res.sendFile('/path/to/document.pdf');
});

app.listen(3000, () => {
console.log('Server running on port 3000');
});

In this example, when a user visits /document, they'll be prompted to download a file named "document.pdf" instead of viewing it in the browser.

Content-Disposition Options

Inline Content

To display content directly in the browser (when possible):

javascript
app.get('/view-image', (req, res) => {
res.setHeader('Content-Disposition', 'inline');
res.setHeader('Content-Type', 'image/jpeg');
res.sendFile('/path/to/image.jpg');
});

Downloadable Content with Custom Filename

To make content download with a custom filename:

javascript
app.get('/download-report', (req, res) => {
const reportDate = new Date().toISOString().split('T')[0]; // YYYY-MM-DD

res.setHeader(
'Content-Disposition',
`attachment; filename="report-${reportDate}.xlsx"`
);

res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.sendFile('/path/to/report.xlsx');
});

Handling Files with Non-ASCII Filenames

When working with filenames containing non-ASCII characters (like accents, non-Latin alphabets, etc.), you should use the filename* parameter with UTF-8 encoding:

javascript
app.get('/download-international', (req, res) => {
const filename = 'résumé-español.pdf'; // Non-ASCII characters
const encodedFilename = encodeURIComponent(filename);

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

res.setHeader('Content-Type', 'application/pdf');
res.sendFile('/path/to/international-document.pdf');
});

Simplified Approach with res.download()

Express provides a convenient res.download() method that sets the appropriate Content-Disposition header for you:

javascript
app.get('/easy-download', (req, res) => {
res.download(
'/path/to/file.pdf', // Path to the file
'custom-filename.pdf', // Custom filename (optional)
(err) => {
if (err) {
// Handle error, but response may be partially sent
console.error('Download error:', err);

// Only call if headers haven't been sent yet
if (!res.headersSent) {
res.status(500).send('Download failed');
}
} else {
// Successfully downloaded
console.log('Download complete');
}
}
);
});

The res.download() method handles the following for you:

  • Sets the Content-Disposition header to "attachment"
  • Sets the appropriate Content-Type based on the file extension
  • Sends the file from the specified path
  • Provides error handling capabilities

Dynamic Content Generation with Content-Disposition

You can also generate content on-the-fly and serve it as a downloadable file:

javascript
const fs = require('fs');
const path = require('path');

app.get('/generate-report', (req, res) => {
// Generate content (in this case, a simple CSV)
const csvContent = 'Name,Email,Score\n' +
'John Doe,[email protected],85\n' +
'Jane Smith,[email protected],92\n' +
'Bob Johnson,[email protected],78';

// Set headers for download
res.setHeader('Content-Disposition', 'attachment; filename="report.csv"');
res.setHeader('Content-Type', 'text/csv');

// Send the generated content
res.send(csvContent);
});

Practical Use Cases

File Download Portal

Here's a more complete example of a file download portal:

javascript
const express = require('express');
const path = require('path');
const fs = require('fs');
const app = express();

// Serve the file download page
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'download-portal.html'));
});

// API to get available files
app.get('/api/files', (req, res) => {
const filesDir = path.join(__dirname, 'files');

fs.readdir(filesDir, (err, files) => {
if (err) {
return res.status(500).json({ error: 'Failed to read files directory' });
}

// Filter out directories and system files
const fileList = files.filter(file => {
const filePath = path.join(filesDir, file);
return fs.statSync(filePath).isFile() && !file.startsWith('.');
});

res.json({ files: fileList });
});
});

// Download endpoint
app.get('/download/:filename', (req, res) => {
const filename = req.params.filename;
const filePath = path.join(__dirname, 'files', filename);

// Validate the file exists
fs.access(filePath, fs.constants.F_OK, (err) => {
if (err) {
return res.status(404).send('File not found');
}

// Set content disposition to force download
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);

// Send the file
res.sendFile(filePath);
});
});

app.listen(3000, () => {
console.log('File download portal running on http://localhost:3000');
});

PDF Report Generator

This example generates a PDF report and serves it to the user:

javascript
const express = require('express');
const PDFDocument = require('pdfkit');
const app = express();

app.get('/report/:userId', async (req, res) => {
const userId = req.params.userId;

try {
// Fetch user data from database (simulated)
const userData = await getUserData(userId);

// Create a PDF document
const doc = new PDFDocument();

// Set Content-Disposition header for download
res.setHeader('Content-Disposition', `attachment; filename="report-${userId}.pdf"`);
res.setHeader('Content-Type', 'application/pdf');

// Pipe the PDF directly to the response
doc.pipe(res);

// Add content to the PDF
doc.fontSize(25).text('User Report', 100, 100);
doc.fontSize(14).text(`User ID: ${userData.id}`, 100, 150);
doc.text(`Name: ${userData.name}`, 100, 180);
doc.text(`Email: ${userData.email}`, 100, 210);
doc.text(`Subscription: ${userData.subscription}`, 100, 240);

// Add a table of activity
doc.text('Recent Activity:', 100, 280);
userData.activities.forEach((activity, i) => {
doc.text(`${activity.date} - ${activity.description}`, 120, 310 + i*30);
});

// Finalize the PDF and end the stream
doc.end();
} catch (error) {
console.error('Error generating report:', error);
res.status(500).send('Error generating report');
}
});

// Mock function to get user data
function getUserData(userId) {
return Promise.resolve({
id: userId,
name: 'Sample User',
email: '[email protected]',
subscription: 'Premium',
activities: [
{ date: '2023-09-15', description: 'Logged in' },
{ date: '2023-09-14', description: 'Updated profile' },
{ date: '2023-09-10', description: 'Purchased subscription' }
]
});
}

app.listen(3000);

Common Pitfalls and Solutions

1. Headers Already Sent Error

javascript
app.get('/document', (req, res) => {
// ❌ Incorrect: Can't set headers after sending response
res.send('Some content');
res.setHeader('Content-Disposition', 'attachment; filename="doc.pdf"');

// ✅ Correct: Set headers before sending response
res.setHeader('Content-Disposition', 'attachment; filename="doc.pdf"');
res.send('Some content');
});

2. Spaces and Special Characters in Filenames

javascript
app.get('/download-file', (req, res) => {
const filename = 'Sales Report 2023 (Q3).pdf';

// ❌ Incorrect: Spaces and special characters not properly handled
res.setHeader('Content-Disposition', `attachment; filename=${filename}`);

// ✅ Correct: Use quotes and encode if necessary
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);

// ✅ Even better: Add filename* parameter for UTF-8 support
const encodedFilename = encodeURIComponent(filename);
res.setHeader(
'Content-Disposition',
`attachment; filename="${filename}"; filename*=UTF-8''${encodedFilename}`
);

res.sendFile('/path/to/file.pdf');
});

Summary

The Content-Disposition header in Express.js provides powerful control over how browsers handle your content. You can:

  • Make browsers display content inline with inline
  • Force browsers to download content with attachment
  • Specify custom filenames for downloaded content
  • Handle international filenames with proper encoding
  • Use Express's built-in res.download() method for simplified file downloads

By mastering Content-Disposition headers, you can create better user experiences in your web applications while ensuring content is delivered exactly as intended.

Additional Resources

Exercise Ideas

  1. Create a file manager API with endpoints to upload and download files with proper Content-Disposition headers.
  2. Build a PDF generator that creates a customized resume and serves it as a downloadable file.
  3. Create an API that serves the same JSON data with different Content-Disposition settings based on query parameters.
  4. Build a CSV export feature for a data table that generates the content dynamically and serves it as a downloadable file.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)