Implementing File Downloads with Express

· 2 min read

Looking at the Express API, for file downloads, the simplest implementation method is as follows

res.download('/report-12345.pdf', 'report.pdf', function(err){
  if (err) {
    // Handle error, but keep in mind the response may be partially-sent
    // so check res.headersSent
  } else {
    // decrement a download credit, etc.
  }
});

However, in actual use, a problem was discovered with this method - when downloading with Apple Safari browser, the file title would be automatically truncated, become garbled, or show question marks.

At first, I was puzzled since Internet Explorer had no issues. After analyzing with Fiddler packet capture, I found that the response header information was problematic - there were duplicate filenames. Different browsers handle duplicate filenames differently, or rather, Safari has issues with duplicate filenames. The res.download approach is highly abstracted after all.

In other words, avoid using such highly abstracted approaches. So how to solve this?

Here’s an alternative approach that isn’t highly abstracted and manually writes response header information. After testing, Safari downloads work perfectly.

    let filename = "Hello, Earthlings Hello, Earthlings Hello, Earthlings Hello, Earthlings.pdf";
    let filePath = path.resolve(__dirname, '..') + '/static/pdf/test.pdf';
    let mimetype = mime.lookup(filePath);
    res.setHeader('Content-Disposition', 'attachment; filename=' + new Buffer(filename).toString('binary'));
    res.setHeader('Content-type', mimetype);
    let filestream = fs.createReadStream(filePath);
    filestream.pipe(res);

Comparing the two approaches, Method 1 is much simpler than Method 2, but currently doesn’t support Safari and IE well. If using Method 2 directly, Edge will have issues, which then involves handling logic for different browsers with mixed Chinese and English filenames.

So the best solution is to handle this based on request headers. For browser detection, you can use the following library:

 const parser = require('ua-parser-js');
 let ua = parser(req.headers['user-agent']);
  if (['Edge', 'Chrome', 'Firefox'].indexOf(ua.browser.name) > -1) {
             res.download(filePath, filename, function (err) {
                     if (err) {
                         logger.error('Error occurred');
                         logger.error(err)
                     }
                     else {
 
                     }
                 }
             );
         }
         else {
             let mimetype = mime.lookup(filePath);
             res.setHeader('Content-type', mimetype);
             if (ua.browser.name == 'IE') {
                 res.setHeader('Content-Disposition', 'attachment; filename=' + encodeURIComponent(filename));
             } /*else if (ua.browser.name == 'Firefox') {
              res.setHeader('Content-Disposition', 'attachment; filename*="utf8\'\'' + encodeURIComponent(filename) + '"');
              } */ else {
                 /* Safari and other non-mainstream browsers can only hope for the best */
                 res.setHeader('Content-Disposition', 'attachment; filename=' + new Buffer(filename).toString('binary'));
             }
             let filestream = fs.createReadStream(filePath);
             filestream.pipe(res);
         }

Browser Support

The above code has been tested and supports the following browsers:

  • IE
  • Chrome
  • Firefox
  • Edge
  • 360 (Fast & Compatible modes)
  • QQ Browser (Fast & Compatible modes)