Middleware phases - initial implementation
Modify the app and router implementation, so that the middleware is executed in order defined by phases. Predefined phases: 'initial', 'session', 'auth', 'parse', 'routes', 'files', 'final' Methods defined via `app.use`, `app.route` and friends are executed as the first thing in 'routes' phase. API usage: app.middleware('initial', compression()); app.middleware('initial:before', serveFavicon()); app.middleware('files:after', loopback.urlNotFound()); app.middleware('final:after', errorHandler()); Middleware flavours: // regular handler function handler(req, res, next) { // do stuff next(); } // error handler function errorHandler(err, req, res, next) { // handle error and/or call next next(err); }
This commit is contained in:
parent
747da886c9
commit
4e1433b519
|
@ -2,7 +2,7 @@
|
|||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var express = require('express');
|
||||
var express = require('./server-app');
|
||||
var proto = require('./application');
|
||||
var fs = require('fs');
|
||||
var ejs = require('ejs');
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
var express = require('express');
|
||||
var merge = require('util')._extend;
|
||||
var PhaseList = require('loopback-phase').PhaseList;
|
||||
var debug = require('debug')('loopback:app');
|
||||
|
||||
var proto = {};
|
||||
|
||||
module.exports = function loopbackExpress() {
|
||||
var app = express();
|
||||
app.__expressLazyRouter = app.lazyrouter;
|
||||
merge(app, proto);
|
||||
return app;
|
||||
};
|
||||
|
||||
/**
|
||||
* Register a middleware handler to be executed in a given phase.
|
||||
* @param {string} name The phase name, e.g. "init" or "routes".
|
||||
* @param {function} handler The middleware handler, one of
|
||||
* `function(req, res, next)` or
|
||||
* `function(err, req, res, next)`
|
||||
* @returns {object} this (fluent API)
|
||||
*/
|
||||
proto.middleware = function(name, handler) {
|
||||
this.lazyrouter();
|
||||
|
||||
var fullName = name;
|
||||
var handlerName = handler.name || '(anonymous)';
|
||||
|
||||
var hook = 'use';
|
||||
var m = name.match(/^(.+):(before|after)$/);
|
||||
if (m) {
|
||||
name = m[1];
|
||||
hook = m[2];
|
||||
}
|
||||
|
||||
var phase = this._requestHandlingPhases.find(name);
|
||||
if (!phase)
|
||||
throw new Error('Unknown middleware phase ' + name);
|
||||
|
||||
var wrapper;
|
||||
if (handler.length === 4) {
|
||||
// handler is function(err, req, res, next)
|
||||
debug('Add error handler %j to phase %j', handlerName, fullName);
|
||||
|
||||
wrapper = function errorHandler(ctx, next) {
|
||||
if (ctx.err) {
|
||||
var err = ctx.err;
|
||||
ctx.err = undefined;
|
||||
handler(err, ctx.req, ctx.res, storeErrorAndContinue(ctx, next));
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// handler is function(req, res, next)
|
||||
debug('Add middleware %j to phase %j', handlerName , fullName);
|
||||
wrapper = function regularHandler(ctx, next) {
|
||||
if (ctx.err) {
|
||||
next();
|
||||
} else {
|
||||
handler(ctx.req, ctx.res, storeErrorAndContinue(ctx, next));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
phase[hook](wrapper);
|
||||
return this;
|
||||
};
|
||||
|
||||
function storeErrorAndContinue(ctx, next) {
|
||||
return function(err) {
|
||||
if (err) ctx.err = err;
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
// Install our custom PhaseList-based handler into the app
|
||||
proto.lazyrouter = function() {
|
||||
var self = this;
|
||||
if (self._router) return;
|
||||
|
||||
self.__expressLazyRouter();
|
||||
|
||||
// Storing the fn in another property of the router object
|
||||
// allows us to call the method with the router as `this`
|
||||
// without the need to use slow `call` or `apply`.
|
||||
self._router.__expressHandle = self._router.handle;
|
||||
|
||||
self._requestHandlingPhases = new PhaseList();
|
||||
self._requestHandlingPhases.add([
|
||||
'initial', 'session', 'auth', 'parse',
|
||||
'routes', 'files', 'final'
|
||||
]);
|
||||
|
||||
// In order to pass error into express router, we have
|
||||
// to pass it to a middleware executed from within the router.
|
||||
// This is achieved by adding a phase-handler that wraps the error
|
||||
// into `req` object and then a router-handler that unwraps the error
|
||||
// and calls `next(err)`.
|
||||
// It is important to register these two handlers at the very beginning,
|
||||
// before any other handlers are added.
|
||||
self.middleware('routes', function wrapError(err, req, res, next) {
|
||||
req.__err = err;
|
||||
next();
|
||||
});
|
||||
|
||||
self.use(function unwrapError(req, res, next) {
|
||||
var err = req.__err;
|
||||
req.__err = undefined;
|
||||
next(err);
|
||||
});
|
||||
|
||||
self.middleware('routes', function runRootHandlers(req, res, next) {
|
||||
self._router.__expressHandle(req, res, next);
|
||||
});
|
||||
|
||||
// Overwrite the original handle() function provided by express,
|
||||
// replace it with our implementation based on PhaseList
|
||||
self._router.handle = function(req, res, next) {
|
||||
var ctx = { req: req, res: res };
|
||||
self._requestHandlingPhases.run(ctx, function(err) {
|
||||
next(err || ctx.err);
|
||||
});
|
||||
};
|
||||
};
|
|
@ -38,7 +38,7 @@
|
|||
"canonical-json": "0.0.4",
|
||||
"debug": "~2.0.0",
|
||||
"ejs": "~1.0.0",
|
||||
"express": "4.x",
|
||||
"express": "^4.10.2",
|
||||
"inflection": "~1.4.2",
|
||||
"loopback-connector-remote": "^1.0.1",
|
||||
"nodemailer": "~1.3.0",
|
||||
|
@ -78,6 +78,7 @@
|
|||
"karma-script-launcher": "~0.1.0",
|
||||
"loopback-boot": "^1.1.0",
|
||||
"loopback-datasource-juggler": "^2.8.0",
|
||||
"loopback-phase": "^1.0.1",
|
||||
"loopback-testing": "~0.2.0",
|
||||
"mocha": "~1.21.4",
|
||||
"serve-favicon": "~2.1.3",
|
||||
|
@ -90,6 +91,7 @@
|
|||
},
|
||||
"browser": {
|
||||
"express": "./lib/browser-express.js",
|
||||
"./lib/server-app.js": "./lib/browser-express.js",
|
||||
"connect": false,
|
||||
"nodemailer": false
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
var path = require('path');
|
||||
var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-app');
|
||||
var http = require('http');
|
||||
var loopback = require('../');
|
||||
var PersistedModel = loopback.PersistedModel;
|
||||
|
||||
|
@ -7,6 +7,102 @@ var describe = require('./util/describe');
|
|||
var it = require('./util/it');
|
||||
|
||||
describe('app', function() {
|
||||
describe.onServer('.middleware(phase, handler)', function() {
|
||||
var app;
|
||||
var steps;
|
||||
|
||||
beforeEach(function setup() {
|
||||
app = loopback();
|
||||
steps = [];
|
||||
});
|
||||
|
||||
it('runs middleware in phases', function(done) {
|
||||
var PHASES = [
|
||||
'initial', 'session', 'auth', 'parse',
|
||||
'routes', 'files', 'final'
|
||||
];
|
||||
|
||||
PHASES.forEach(function(name) {
|
||||
app.middleware(name, namedHandler(name));
|
||||
});
|
||||
app.use(namedHandler('main'));
|
||||
|
||||
executeHandlers(function(err) {
|
||||
if (err) return done(err);
|
||||
expect(steps).to.eql([
|
||||
'initial', 'session', 'auth', 'parse',
|
||||
'main', 'routes', 'files', 'final'
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('supports "before:" and "after:" prefixes', function(done) {
|
||||
app.middleware('routes:before', namedHandler('routes:before'));
|
||||
app.middleware('routes:after', namedHandler('routes:after'));
|
||||
app.use(namedHandler('main'));
|
||||
|
||||
executeHandlers(function(err) {
|
||||
if (err) return done(err);
|
||||
expect(steps).to.eql(['routes:before', 'main', 'routes:after']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('injects error from previous phases into the router', function(done) {
|
||||
var expectedError = new Error('expected error');
|
||||
|
||||
app.middleware('initial', function(req, res, next) {
|
||||
steps.push('initial');
|
||||
next(expectedError);
|
||||
});
|
||||
|
||||
// legacy solution for error handling
|
||||
app.use(function errorHandler(err, req, res, next) {
|
||||
expect(err).to.equal(expectedError);
|
||||
steps.push('error');
|
||||
next();
|
||||
});
|
||||
|
||||
executeHandlers(function(err) {
|
||||
if (err) return done(err);
|
||||
expect(steps).to.eql(['initial', 'error']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('passes unhandled error to callback', function(done) {
|
||||
var expectedError = new Error('expected error');
|
||||
|
||||
app.middleware('initial', function(req, res, next) {
|
||||
next(expectedError);
|
||||
});
|
||||
|
||||
executeHandlers(function(err) {
|
||||
expect(err).to.equal(expectedError);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
function namedHandler(name) {
|
||||
return function(req, res, next) {
|
||||
steps.push(name);
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function executeHandlers(callback) {
|
||||
var server = http.createServer(function(req, res) {
|
||||
app.handle(req, res, callback);
|
||||
});
|
||||
|
||||
request(server)
|
||||
.get('/test/url')
|
||||
.end(function(err) {
|
||||
if (err) return callback(err);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('app.model(Model)', function() {
|
||||
var app, db;
|
||||
|
|
Loading…
Reference in New Issue