Search code examples
node.jsreactjsfs

How to make sure that the file is downloaded and then return a response to client side?


I have to download a json file using nodejs and react. So, when the user clicks on download button, a request is sent to nodejs, then I make an http request passing the user, pass, and the url to download json file on the server and then download this file on client side

The file download on server side takes almost 5 min, the problem is that I'm getting an network error before the download file is finished:

GET http://127.0.0.1:4000/downloadFile net::ERR_EMPTY_RESPONSE
Error: Network Error
    at createError (createError.js:17)
    at XMLHttpRequest.handleError (xhr.js:80)

When download button is clicked this function on react side is called:

downloadFile() {
    this.setState({ buttonDownloadText: 'Wait ...' })
    axios.get(process.env.REACT_APP_HOST + '/downloadFile')
      .then(resp => {            
        window.open(process.env.REACT_APP_HOST + '/download?namefile=' + resp.data.fileName, '_self')           

        this.setState({ buttonDownloadText: 'Start download' })    

      })
      .catch((error) => {
        console.log(error) //here is where the Network Error is returned
        this.setState({ buttonDownloadText: 'Error. Try again' })
      })
  }

Server side:

  const http = require('http');
  const fs = require('fs');

    app.get('/downloadFile', function (req, res, next) {
        try {
            const headers = new Headers();
            const URL = env.URL_API + '/api/downloadFile'
            const DOWNLOADURL = env.URL_API + '/api/download'
            headers.set('Authorization', 'Basic ' + base64.encode(username + ":" + password));

            fetch(URL, {
                method: 'GET',
                headers: headers,
            })
                .then(r => r.json())
                .then(data => 
                    fetch(DOWNLOADURL + '/' + data.fileName, {
                        method: 'GET',
                        headers: headers,
                    }).then(result => {   
                                const url = 'http://user:pass@url' + data.fileName
                                const arq = __dirname + '/download/' + data.fileName

                                var file = fs.createWriteStream(arq);
                                http.get(url, function (response) {
                                    response.pipe(file);
                                    file.on('finish', () => file.close(() => res.status(200).send({ file: data.fileName });)) //I have to return the file name to react function
                                });


                            }).catch(error => {
                                logger.error(`${error}`);
                            })
                    })
                        .catch(error => {
                            logger.error(`${error.status || 500} - ${error} - ${req.originalUrl} - ${req.method} - ${req.ip}`);
                            res.sendStatus(500)
                        })
               ....
    });

downloadFile function must return the fileName, so I call download route to make a download on clint side

app.get('/download', function (req, res) {
    try {
        const file = __dirname + '/download/' + req.query.namefile;
        res.download(file);
    } catch (error) {
        logger.error(`${error.status || 500} - ${error} - ${req.originalUrl} - ${req.method} - ${req.ip}`);
    }
});

So, my point is, Why my downloadFile route is not waiting the download file finish to return it res.status(200).send({ file: data.fileName })?


Solution

  • As browsers cannot be forced to wait for a long response from the server, I suggest a different approach: upload the file to the cloud (e.g. AWS or Firebase) and return a link to the client.

    If using Firebase (or another real time database) the server can set a flag in the db as the file upload is finished. Therefore client gets notified asynchronously when the file is available for the download.