diff --git a/HISTORY.md b/HISTORY.md index 7a388b2..d9f6e64 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,8 @@ 2.x === + * Add basic support for returned, rejected Promises + - Rejected Promises from middleware functions `next(error)` * Drop support for Node.js below 0.10 * deps: debug@3.1.0 - Add `DEBUG_HIDE_DATE` environment variable diff --git a/README.md b/README.md index 15d9b66..6c5bddf 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,11 @@ format is with three parameters - "req", "res" and "next". - `res` - This is a [HTTP server response](https://nodejs.org/api/http.html#http_class_http_serverresponse) instance. - `next` - Calling this function that tells `router` to proceed to the next matching middleware or method handler. It accepts an error as the first argument. +The function can optionally return a `Promise` object. If a `Promise` object +is returned from the function, the router will attach an `onRejected` callback +using `.then`. If the promise is rejected, `next` will be called with the +rejected value, or an error if the value is falsy. + Middleware and method handlers can also be defined with four arguments. When the function has four parameters defined, the first argument is an error and subsequent arguments remain, becoming - "err", "req", "res", "next". These diff --git a/lib/layer.js b/lib/layer.js index 60a737f..c012165 100644 --- a/lib/layer.js +++ b/lib/layer.js @@ -66,7 +66,15 @@ Layer.prototype.handle_error = function handle_error(error, req, res, next) { } try { - fn(error, req, res, next) + // invoke function + var ret = fn(error, req, res, next) + + // wait for returned promise + if (isPromise(ret)) { + ret.then(null, function (error) { + next(error || new Error('Rejected promise')) + }) + } } catch (err) { next(err) } @@ -90,7 +98,15 @@ Layer.prototype.handle_request = function handle(req, res, next) { } try { - fn(req, res, next) + // invoke function + var ret = fn(req, res, next) + + // wait for returned promise + if (isPromise(ret)) { + ret.then(null, function (error) { + next(error || new Error('Rejected promise')) + }) + } } catch (err) { next(err) } @@ -178,3 +194,17 @@ function decode_param(val){ throw err } } + +/** + * Returns true if the val is a Promise. + * + * @param {*} val + * @return {boolean} + * @private + */ + +function isPromise (val) { + return val && + typeof val === 'object' && + typeof val.then === 'function' +} diff --git a/test/route.js b/test/route.js index 47c8ff5..d680272 100644 --- a/test/route.js +++ b/test/route.js @@ -11,6 +11,8 @@ var request = utils.request var shouldHitHandle = utils.shouldHitHandle var shouldNotHitHandle = utils.shouldNotHitHandle +var describePromises = global.Promise ? describe : describe.skip + describe('Router', function () { describe('.route(path)', function () { it('should return a new route', function () { @@ -467,6 +469,141 @@ describe('Router', function () { }) }) + describePromises('promise support', function () { + it('should pass rejected promise value', function (done) { + var router = new Router() + var route = router.route('/foo') + var server = createServer(router) + + route.all(function createError (req, res, next) { + return Promise.reject(new Error('boom!')) + }) + + route.all(helloWorld) + + route.all(function handleError (err, req, res, next) { + res.statusCode = 500 + res.end('caught: ' + err.message) + }) + + request(server) + .get('/foo') + .expect(500, 'caught: boom!', done) + }) + + it('should pass rejected promise without value', function (done) { + var router = new Router() + var route = router.route('/foo') + var server = createServer(router) + + route.all(function createError (req, res, next) { + return Promise.reject() + }) + + route.all(helloWorld) + + route.all(function handleError (err, req, res, next) { + res.statusCode = 500 + res.end('caught: ' + err.message) + }) + + request(server) + .get('/foo') + .expect(500, 'caught: Rejected promise', done) + }) + + it('should ignore resolved promise', function (done) { + var router = new Router() + var route = router.route('/foo') + var server = createServer(router) + + route.all(function createError (req, res, next) { + saw(req, res) + return Promise.resolve('foo') + }) + + route.all(function () { + done(new Error('Unexpected route invoke')) + }) + + request(server) + .get('/foo') + .expect(200, 'saw GET /foo', done) + }) + + describe('error handling', function () { + it('should pass rejected promise value', function (done) { + var router = new Router() + var route = router.route('/foo') + var server = createServer(router) + + route.all(function createError (req, res, next) { + return Promise.reject(new Error('boom!')) + }) + + route.all(function handleError (err, req, res, next) { + return Promise.reject(new Error('caught: ' + err.message)) + }) + + route.all(function handleError (err, req, res, next) { + res.statusCode = 500 + res.end('caught again: ' + err.message) + }) + + request(server) + .get('/foo') + .expect(500, 'caught again: caught: boom!', done) + }) + + it('should pass rejected promise without value', function (done) { + var router = new Router() + var route = router.route('/foo') + var server = createServer(router) + + route.all(function createError (req, res, next) { + return Promise.reject(new Error('boom!')) + }) + + route.all(function handleError (err, req, res, next) { + return Promise.reject() + }) + + route.all(function handleError (err, req, res, next) { + res.statusCode = 500 + res.end('caught again: ' + err.message) + }) + + request(server) + .get('/foo') + .expect(500, 'caught again: Rejected promise', done) + }) + + it('should ignore resolved promise', function (done) { + var router = new Router() + var route = router.route('/foo') + var server = createServer(router) + + route.all(function createError (req, res, next) { + return Promise.reject(new Error('boom!')) + }) + + route.all(function handleError (err, req, res, next) { + res.statusCode = 500 + res.end('caught: ' + err.message) + return Promise.resolve('foo') + }) + + route.all(function () { + done(new Error('Unexpected route invoke')) + }) + + request(server) + .get('/foo') + .expect(500, 'caught: boom!', done) + }) + }) + }) + describe('path', function () { describe('using ":name"', function () { it('should name a capture group', function (done) { diff --git a/test/router.js b/test/router.js index ea3d73c..0bb89a4 100644 --- a/test/router.js +++ b/test/router.js @@ -12,6 +12,8 @@ var request = utils.request var shouldHitHandle = utils.shouldHitHandle var shouldNotHitHandle = utils.shouldNotHitHandle +var describePromises = global.Promise ? describe : describe.skip + describe('Router', function () { it('should return a function', function () { assert.equal(typeof Router(), 'function') @@ -690,6 +692,118 @@ describe('Router', function () { }) }) + describePromises('promise support', function () { + it('should pass rejected promise value', function (done) { + var router = new Router() + var server = createServer(router) + + router.use(function createError (req, res, next) { + return Promise.reject(new Error('boom!')) + }) + + router.use(sawError) + + request(server) + .get('/') + .expect(200, 'saw Error: boom!', done) + }) + + it('should pass rejected promise without value', function (done) { + var router = new Router() + var server = createServer(router) + + router.use(function createError (req, res, next) { + return Promise.reject() + }) + + router.use(sawError) + + request(server) + .get('/') + .expect(200, 'saw Error: Rejected promise', done) + }) + + it('should ignore resolved promise', function (done) { + var router = new Router() + var server = createServer(router) + + router.use(function createError (req, res, next) { + saw(req, res) + return Promise.resolve('foo') + }) + + router.use(function () { + done(new Error('Unexpected middleware invoke')) + }) + + request(server) + .get('/foo') + .expect(200, 'saw GET /foo', done) + }) + + describe('error handling', function () { + it('should pass rejected promise value', function (done) { + var router = new Router() + var server = createServer(router) + + router.use(function createError (req, res, next) { + return Promise.reject(new Error('boom!')) + }) + + router.use(function handleError (err, req, res, next) { + return Promise.reject(new Error('caught: ' + err.message)) + }) + + router.use(sawError) + + request(server) + .get('/') + .expect(200, 'saw Error: caught: boom!', done) + }) + + it('should pass rejected promise without value', function (done) { + var router = new Router() + var server = createServer(router) + + router.use(function createError (req, res, next) { + return Promise.reject() + }) + + router.use(function handleError (err, req, res, next) { + return Promise.reject(new Error('caught: ' + err.message)) + }) + + router.use(sawError) + + request(server) + .get('/') + .expect(200, 'saw Error: caught: Rejected promise', done) + }) + + it('should ignore resolved promise', function (done) { + var router = new Router() + var server = createServer(router) + + router.use(function createError (req, res, next) { + return Promise.reject(new Error('boom!')) + }) + + router.use(function handleError (err, req, res, next) { + sawError(err, req, res, next) + return Promise.resolve('foo') + }) + + router.use(function () { + done(new Error('Unexpected middleware invoke')) + }) + + request(server) + .get('/foo') + .expect(200, 'saw Error: boom!', done) + }) + }) + }) + describe('req.baseUrl', function () { it('should be empty', function (done) { var router = new Router()