Reading and returning multiple files in Node.js using fs.readFile


Question

I'm writing a simple request handler to return a pair of css files. Using fs.readFileSync this was easy. However, I'm having difficulty accomplishing the same task using the async version of readFile. Below is my code. Having my response.write() method calls split among two different callbacks seems to be problematic. Can someone point out what I've done wrong? Interestingly this code works if I put response.end() inside of the first else statement. However, that creates a problem in that the second css file does not get returned (because response.end() has already been fired).

function css(response) {

  response.writeHead(200, {"Content-Type": "text/css"});

  fs.readFile('css/bootstrap.css', function(error, content){
    if(error){
      console.log(error);
    }
    else{
      response.write(content);
    }
  });
  fs.readFile('css/bootstrap-responsive.css', function(error, content){
    if(error){
      console.log(error);
    }
    else{
      response.write(content)
    }
  });
  response.end();
}
1
19
2/26/2012 1:16:14 AM

Accepted Answer

The primary issue with what you have is that response.end() gets called right away. You need to only call it after the files have done their response.write calls.

The easiest way would be to use a control flow library. Managing multiple asynchronous callbacks is generally complicated.

https://github.com/joyent/node/wiki/modules#wiki-async-flow

I'm going to use the async library because it's the one I know best.

var fs = require('fs');
var async = require('async');

function css(response) {
  response.writeHead(200, {"Content-Type": "text/css"});

  async.eachSeries(
    // Pass items to iterate over
    ['css/bootstrap.css', 'css/bootstrap-responsive.css'],
    // Pass iterator function that is called for each item
    function(filename, cb) {
      fs.readFile(filename, function(err, content) {
        if (!err) {
          response.write(content);
        }

        // Calling cb makes it go to the next item.
        cb(err);
      });
    },
    // Final callback after each item has been iterated over.
    function(err) {
      response.end()
    }
  );
}

If you want to accomplish this without a library, or just want another way, this is how I would do it more directly. Basically you keep a count and call end once both file reads have finished.

function css(response) {
  response.writeHead(200, {"Content-Type": "text/css"});

  var count = 0;
  var handler = function(error, content){
    count++;
    if (error){
      console.log(error);
    }
    else{
      response.write(content);
    }

    if (count == 2) {
      response.end();
    }
  }

  fs.readFile('css/bootstrap.css', handler);
  fs.readFile('css/bootstrap-responsive.css', handler);
}
31
10/30/2014 12:46:37 AM

You can simply rely on html5 Promise. The code can be as simple as follows:

var promises= ['file1.css', 'file2.css'].map(function(_path){
    return new Promise(function(_path, resolve, reject){
        fs.readFile(_path, 'utf8', function(err, data){
            if(err){
               console.log(err);
               resolve("");    //following the same code flow
            }else{
               resolve(data);
            }
        });
    }.bind(this, _path));
});

Promise.all(promises).then(function(results){
    //Put your callback logic here
    response.writeHead(200, {"Content-Type": "text/css"});
    results.forEach(function(content){response.write(content)});
    response.end();
});

Licensed under: CC-BY-SA with attribution
Not affiliated with: Stack Overflow
Icon