0

EDIT:

The title has been changed so that anyone can suggest an alternative solution that achieves the same result with similar technology and platform. Not necessary has to be res.attachment.

I was trying to achieve a force download of PDF file by cross-origin URL. The code seems to work as expected but the downloaded file is ZERO BYTES, why?

On server:

app.get("/download", (req, res) => {
    res.type("application/octet-stream");
    // Note that the PDF URL is Cross-Origin.
    res.attachment("https://cross-origin-domain-name.com/downloads/fiename.pdf");
    res.send();
});

On HTML:

<a class="a-very-big-button" href="/download">Download PDF</a>

Do I miss anything? I did try out many other options like res.download() and readStream.pipe(res) methods, but most of them require the files to be on the same server. For my app, I need to help my clients to offer a download PDF button based on the URL submitted by them, which could be on their web server. Any advice would be appreciated! Thank you.

Antonio Ooi
  • 1,601
  • 1
  • 18
  • 32

1 Answers1

1

res.attachment does take a string as its only argument, but that string is used as a hint to the browser what the filename should be if the user decides to save the file. It does not allow you to specify a URL or filename to fetch.

Because you're not sending any data (res.send() without a Buffer or .write() calls), just a suggestion as to what the filename should be, the download is 0 bytes.

What you could do is pipe a HTTP request to res, which will have your server download and forward the file. The file will not be cached on your server and will 'cost' both upload and download capacity (but no storage).


An example on how to pipe a HTTPS request to a response.

Instead of Node's built-in https.request you could use many other libraries. Most of them support streaming files. These libraries can make it easier to handle errors.

const express = require('express');
const https = require('https');

const app = express();
const url = 'https://full-url-to-your/remote-file.pdf';
const headerAllowList = [
  'content-type', 'content-length', 'last-modified', 'etag'
];

app.use('/', async (req, res, next) => {
  // Create a HTTPS request
  const externalRequest = https.request(url, {
    headers: {
      // You can add headers like authorization or user agent strings here.
      // Accept: '*/*',
      // 'User-Agent': '',
    },
  }, (externalResponse) => {
    // This callback won't start until `.end()` is called.

    // To make life easier on ourselves, we can copy some of the headers
    // that the server sent your Node app and pass them along to the user.
    headerAllowList
      .filter(header => header in externalResponse.headers)
      .forEach(header => res.set(header, externalResponse.headers[header]));

    // If we didn't have content-type in the above headerAllowList, 
    // you could manually tell browser to expect a PDF file.
    // res.set('Content-Type', 'application/pdf');

    // Suggest a filename
    res.attachment('some-file.pdf');

    // Start piping the ReadableStream to Express' res.
    externalResponse.pipe(res);
  });

  externalRequest.on('error', (err) => {
    next(err);
  });

  // Start executing the HTTPS request
  externalRequest.end();
});
app.listen(8000);

If you visit localhost:8000 you'll be served a PDF with a save-file dialog with the suggested filename, served from the specified URL.

RickN
  • 12,537
  • 4
  • 24
  • 28
  • Thanks for the reply! But unfortunately, I can't afford to have all my clients' PDF files to be downloaded to my server because that may cost me a lot on Google Cloud Platform (GCP). – Antonio Ooi Sep 28 '21 at 11:41
  • It will cost egress and ingress bandwidth costs, but not any storage fees (because, as mentioned, there's no caching). The only other option you have is to simply linking to the file with `` or do a 301 redirect. – RickN Sep 28 '21 at 12:46
  • The `download` attribute is only for `same-origin`. By the way, I couldn't get [the suggested link](https://stackoverflow.com/a/18433513) to work. Since `request` has deprecated, I used the `fetch` answer instead, but still not working. Is possible for you to add the full code to your existing answer? Honestly speaking, I not very good in `ExpressJS`. I really need the `Save as` dialog box to pop-up and download the PDF file. Thanks! – Antonio Ooi Sep 28 '21 at 12:58
  • 1
    I've added an example that doesn't use external libraries other than Express. This will serve up a PDF file with a Save As dialog, with a suggested file name & as a bonus it will preserve headers like `Content-Length`. – RickN Sep 28 '21 at 14:45
  • Is this parameter important: `{ headers: { // You can add headers like authorization or user agent strings here. // Accept: '*/*', // 'User-Agent': '', }, }`? Can I remove it? I have no idea what headers to add, but I thought we already have the `headerAllowList` needed by us? – Antonio Ooi Sep 28 '21 at 15:00
  • 1
    There's outgoing (request: your Node app to the remote server) and incoming (response: the remote server to your Node app) headers. The ones you quote are what those sent from your node app to the other server. The remote server might require an `Authorization` header, a `User-Agent` or something else. If it does, you can add them there or leave it blank if not. (Edit: and, of course, there's also the headers your Node app sends to the user. I hope the comments make it clear enough what's-what!) – RickN Sep 28 '21 at 15:03
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/237607/discussion-between-antonio-ooi-and-rickn). – Antonio Ooi Sep 28 '21 at 15:30