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 possibleattachment
: 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:
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):
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:
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:
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:
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:
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:
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:
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
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
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
- MDN Web Docs: Content-Disposition Header
- Express.js Documentation on res.download()
- RFC 6266: Use of the Content-Disposition Header Field
Exercise Ideas
- Create a file manager API with endpoints to upload and download files with proper Content-Disposition headers.
- Build a PDF generator that creates a customized resume and serves it as a downloadable file.
- Create an API that serves the same JSON data with different Content-Disposition settings based on query parameters.
- 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! :)