12

I've had no trouble testing my own route handlers but in this case I want to test express's static handler. I can't for the life of me figure out why it's hanging. Clearly there's some callback I'm missing or some event I need to emit.

I tried to make the smallest example I could.

var events = require('events');
var express = require('express');
var stream = require('stream');
var util = require('util');

function MockResponse(callback) {
  stream.Writable.call(this);
  this.headers = {};
  this.statusCode = -1;
  this.body = undefined;

  this.setHeader = function(key, value) {
    this.headers[key] = value;
  }.bind(this);

  this.on('finish', function() {
    console.log("finished response");
    callback();
  });
};

util.inherits(MockResponse, stream.Writable);

MockResponse.prototype._write = function(chunk, encoding, done) {
  if (this.body === undefined) {
    this.body = "";
  }
  this.body += chunk.toString(encoding !== 'buffer' ? encoding : undefined);
  done();
};

function createRequest(req) {
  var emitter = new events.EventEmitter();
  req.on = emitter.on.bind(emitter);
  req.once = emitter.once.bind(emitter);
  req.addListener = emitter.addListener.bind(emitter);
  req.emit = emitter.emit.bind(emitter);
  return req;
};

describe('test', function() {

  var app;

  before(function() {
    app = express();
    app.use(express.static(__dirname));
  });

  it('gets test.js', function(done) {

    var req = createRequest({
        url: "http://foo.com/test.js",
        method: 'GET',
        headers: {
        },
    });
    var res = new MockResponse(responseDone);
    app(req, res);

    function responseDone() {
      console.log("done");
      done();
    }

  });

});

Setup,

mkdir foo
cd foo
mkdir test
cat > test/test.js   # copy and paste code above
^D
npm install express
npm install mocha
node node_modules/mocha/bin/mocha --recursive

it just times out.

What am I missing?

I also tried making the request a Readable stream. No change

var events = require('events');
var express = require('express');
var stream = require('stream');
var util = require('util');

function MockResponse(callback) {
  stream.Writable.call(this);
  this.headers = {};
  this.statusCode = -1;
  this.body = undefined;

  this.setHeader = function(key, value) {
    this.headers[key] = value;
  }.bind(this);

  this.on('finish', function() {
    console.log("finished response");
    callback();
  });
};

util.inherits(MockResponse, stream.Writable);

MockResponse.prototype._write = function(chunk, encoding, done) {
  if (this.body === undefined) {
    this.body = "";
  }
  this.body += chunk.toString(encoding !== 'buffer' ? encoding : undefined);
  done();
};

function MockMessage(req) {
  stream.Readable.call(this);
  var self = this;
  Object.keys(req).forEach(function(key) {
    self[key] = req[key];
  });
}

util.inherits(MockMessage, stream.Readable);

MockMessage.prototype._read = function() {
  this.push(null);
};


describe('test', function() {

  var app;

  before(function() {
    app = express();
    app.use(express.static(__dirname));
  });

  it('gets test.js', function(done) {

    var req = new MockMessage({
        url: "http://foo.com/test.js",
        method: 'GET',
        headers: {
        },
    });
    var res = new MockResponse(responseDone);
    app(req, res);

    function responseDone() {
      console.log("done");
      done();
    }

  });

});

I've still been digging. Look inside static-server I see it creates a Readable stream by calling fs.createReadStream. It does effectively

var s = fs.createReadStream(filename);
s.pipe(res);

So trying that myself works just fine

  it('test stream', function(done) {
    var s = fs.createReadStream(__dirname + "/test.js");
    var res = new MockResponse(responseDone);
    s.pipe(res);

    function responseDone() {
      console.log("done");
      done();
    }    
  });

I thought maybe it's something about express waiting for the input stream to finish but that doesn't seem to be it either. If I consume the mock input stream with the response it works just fine

  it('test msg->res', function(done) {
    var req = new MockMessage({});
    var res = new MockResponse(responseDone);
    req.pipe(res);

    function responseDone() {
      console.log("done");
      done();
    }    
  });

Any insight what I might be missing would be helpful

Note: while suggestions for 3rd party mocking libraries are appreciated I'm still really looking to understand what I'm missing to do it myself. Even if I eventually switch to some library I still want to know why this isn't working.

2
  • My guess is it's the before running async. Maybe try using the callback to wait for it to complete? Commented Sep 16, 2015 at 1:32
  • It's not the before running async. That part completes just fine Commented Sep 16, 2015 at 9:16

2 Answers 2

11
+300

I found two issues that prevent the finish callback from being executed.

  1. serve-static uses send module which is used to create file readstream from the path and pipe it to res object. But that module uses on-finished module which checks if finished attribute is set to false in response object, otherwise it destroys the file readstream. So filestream never gets a chance to emit data event.

  2. express initialization overwrites the response object prototype. So the default stream methods like end() method is overwritten by http response prototype:

    exports.init = function(app){
      return function expressInit(req, res, next){
        ...
        res.__proto__ = app.response;
        ..
      };
    };
    

    To prevent this, I added another middleware right before static middleware to reset it back to MockResponse prototype:

    app.use(function(req, res, next){
      res.__proto__ = MockResponse.prototype; //change it back to MockResponse prototype
      next();
    });
    

Here are the changes made to make it work with MockResponse:

...
function MockResponse(callback) {
  ...
  this.finished = false; // so `on-finished` module doesn't emit finish event prematurely

  //required because of 'send' module
  this.getHeader = function(key) {
    return this.headers[key];
  }.bind(this);
  ...
};

...
describe('test', function() {

  var app;

  before(function() {
    app = express();

    //another middleware to reset the res object
    app.use(function(req, res, next){
      res.__proto__ = MockResponse.prototype;
      next();
    });

    app.use(express.static(__dirname));
  });

  ...

});

EDIT:

As @gman pointed out, it is possible to use direct property instead of prototype method. In that case the extra middleware to overwrite prototype isn't necessary:

function MockResponse(callback) {
  ...
  this.finished = false; // so `on-finished` module doesn't emit finish event prematurely

  //required because of 'send' module
  this.getHeader = function(key) {
     return this.headers[key];
  }.bind(this);

  ...

  //using direct property for _write, write, end - since all these are changed when prototype is changed
  this._write = function(chunk, encoding, done) {
    if (this.body === undefined) {
      this.body = "";
    }
    this.body += chunk.toString(encoding !== 'buffer' ? encoding : undefined);
    done();
  };

  this.write = stream.Writable.prototype.write;
  this.end = stream.Writable.prototype.end;

};
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks. I still ran into all kinds of issues which I don't fully understand but at least I got something to work. As one examples for I change 'write_' do a direct property then I shouldn't have to care about express's prototype change but that breaks because somewhere else express expects the object really is a ServerResponse inherited from OutgoingMessage. Why it works when you put the prototype back tho I don't get
Yes, direct property should work. How about other methods of the stream, like end ? did you make it direct property? Because, when I tested I actually got an error on end method not on _write.
3

It appears my answer is not complete. For some reason the app works only if the file is not found. First thing to debug is do the following in your shell (or cmd):

export DEBUG=express:router,send

then run the test, you'll get more info.

Meanwhile I am still looking into this, for now, ignore my answer below.

----------- ignore this till I verify that it does work -----------

It seems like express static does not favor the absolute path you give it (__dirname).

Try:

app.use(express.static('.'));

and it will work. Note that your current dir for the mocha runner is 'test/'

I have to admit this is quite a mistery. I tried 'fulling' it by doing:

app.use(express.static(__dirname + '/../test')

but still it didn't work. Even specifying a full path did not solve this. Strange.

2 Comments

I found out that most modules have that debug option. I suggest you search your node_modules directory for require('debug') and turn on all debug options. It might give you an insight into what goes on. I did notice that express mentions it sets env to development, it might have something to do with it. The list of debug flags I used is: DEBUG=express:router,send,finalhandler,express:application,express:router:layer,mocha:runnable,express:router:route,mocha:runnable,mocha:runner,express:view One last thing, you can go into the node_modules dir and add in the files debug messages
Also, the static uses a module called send. If you look into it, you'd see there is a comment about how messy it is and that it requires refactoring.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.