I'm playing around with domains and clusters in node with express and got into this circumstance. I have a cluster that spawns a worker per core, for each worker i create a express-server that uses a domain-per-request strategy to handle errors.
The solution given below is working fine as is but when explicitly adding the request and response objects to the domain the error middleware stops being called. I have no idea why this behaviour is introduced.
Questions:
Thanks in advance!
My app.js:
var express = require('express')
, http = require('http')
, path = require('path')
, domain = require('domain')
, cluster = require('cluster')
, http = require('http')
, numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
// fork workers
for (var i = 0; i < numCPUs; i++) {
cluster.fork();
}
// when a worker dies create a new one
cluster.on('exit', function(worker, code, signal) {
cluster.fork();
});
} else {
var app = express();
//domains
app.use(function domainMiddleware(req, res, next) {
var reqDomain = domain.create();
res.on('close', function () {
reqDomain.dispose();
});
res.on('finish', function () {
reqDomain.dispose();
});
reqDomain.on('error', function (err) {
reqDomain.dispose();
// delegate to express error-middleware
next(err);
});
// Adding the request and response objects to the domain
// makes the express error-middleware to not being called.
// reqDomain.add(req);
// reqDomain.add(res);
reqDomain.run(next);
});
// all environments
app.set('port', process.env.PORT || 3000);
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.favicon());
//app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
app.use(express.static(path.join(__dirname, 'public')));
// for testing which cluster that serves the request
app.get('/', function(req, res, next) {
res.json(200, { id: cluster.worker.id });
});
app.get('/error', function(req, res, next) {
var fs = require('fs');
// intentionally force an error
fs.readFile('', process.domain.intercept(function(data) {
// when using intercept we dont need this line anymore
//if (err) throw err;
res.send(data);
}));
});
app.use(function(err, req, res, next) {
console.log('ERROR MIDDLEWARE', err);
res.writeHeader(500, {'Content-Type' : "text/html"});
res.write("<h1>" + err.name + "</h1>");
res.end("<p>" + err.message + "</p>");
});
http.createServer(app).listen(app.get('port'), function(){
console.log('Express server listening on port ' + app.get('port'));
});
}
This is due to the call to reqDomain.dispose(). From the dispose documentation:
The dispose method destroys a domain, and makes a best effort attempt to clean up any and all IO that is associated with the domain. Streams are aborted, ended, closed, and/or destroyed. Timers are cleared. Explicitly bound callbacks are no longer called. Any error events that are raised as a result of this are ignored.
Once req and res are added to the domain, disposing the domain ends/closes/destroys them. You can test this by explicitly sending some output to res inside the reqDomain.on('error') callback, but before disposing the domain.
Simply moving the call to next up a line seems to fix the problem:
reqDomain.on('error', function (err) {
next(err);
reqDomain.dispose();
});
As for process.domain, I've not seen this before and was really surprised that it worked. I looked into the source code and found the following:
Domain.prototype.enter = function() {
if (this._disposed) return;
// note that this might be a no-op, but we still need
// to push it onto the stack so that we can pop it later.
exports.active = process.domain = this;
stack.push(this);
};
So, it appears that process.domain is always the "latest" domain. Personally, I would probably attach the domain to the req object or something, so that you can be more explicit about which domain should be handling the error (although in practice it may be that process.domain is always the domain you're looking for).