UNPKG

6.43 kBJavaScriptView Raw
1'use strict';
2
3var assert = require('assert-plus');
4var once = require('once');
5var customErrorTypes = require('./errorTypes');
6
7module.exports = Chain;
8
9/**
10 * Create a new middleware chain
11 *
12 * @public
13 * @class Chain
14 * @param {Object} [options] - options
15 * @param {Boolean} [options.onceNext=false] - Prevents calling next multiple
16 * times
17 * @param {Boolean} [options.strictNext=false] - Throws error when next() is
18 * called more than once, enables onceNext option
19 * @example
20 * var chain = new Chain();
21 * chain.add(function (req, res, next) { next(); })
22 * // chain.add(function (req, res, next) { next(new Error('Foo')); })
23 * // chain.add(function (req, res, next) { next(false); })
24 *
25 * http.createServer((req, res) => {
26 * chain.run(req, res, function done(err) {
27 * res.end(err ? err.message : 'hello world');
28 * });
29 * })
30 */
31function Chain(options) {
32 assert.optionalObject(options, 'options');
33 options = options || {};
34 assert.optionalBool(options.onceNext, 'options.onceNext');
35 assert.optionalBool(options.strictNext, 'options.strictNext');
36
37 this.onceNext = !!options.onceNext;
38 this.strictNext = !!options.strictNext;
39
40 // strictNext next enforces onceNext
41 if (this.strictNext) {
42 this.onceNext = true;
43 }
44
45 this._stack = [];
46 this._once = this.strictNext === false ? once : once.strict;
47}
48
49/**
50 * Public methods.
51 * @private
52 */
53
54/**
55 * Get handlers of a chain instance
56 *
57 * @memberof Chain
58 * @instance
59 * @returns {Function[]} handlers
60 */
61Chain.prototype.getHandlers = function getHandlers() {
62 return this._stack;
63};
64
65/**
66 * Utilize the given middleware `handler`
67 *
68 * @public
69 * @memberof Chain
70 * @instance
71 * @param {Function} handler - handler
72 * @returns {undefined} no return value
73 */
74Chain.prototype.add = function add(handler) {
75 assert.func(handler);
76 var handlerId = handler._identifier || handler._name || handler.name;
77 if (handler.length <= 2) {
78 // arity <= 2, must be AsyncFunction
79 assert.equal(
80 handler.constructor.name,
81 'AsyncFunction',
82 `Handler [${handlerId}] is missing a third argument (the ` +
83 '"next" callback) but is not an async function. Middleware ' +
84 'handlers can be either async/await or callback-based.' +
85 'Callback-based (non-async) handlers should accept three ' +
86 'arguments: (req, res, next). Async handler functions should ' +
87 'accept maximum of 2 arguments: (req, res).'
88 );
89 } else {
90 // otherwise shouldn't be AsyncFunction
91 assert.notEqual(
92 handler.constructor.name,
93 'AsyncFunction',
94 `Handler [${handlerId}] accepts a third argument (the 'next" ` +
95 'callback) but is also an async function. Middleware ' +
96 'handlers can be either async/await or callback-based. Async ' +
97 'handler functions should accept maximum of 2 arguments: ' +
98 '(req, res). Non-async handlers should accept three ' +
99 'arguments: (req, res, next).'
100 );
101 }
102
103 // _name is assigned in the server and router
104 handler._name = handler._name || handler.name;
105
106 // add the middleware
107 this._stack.push(handler);
108};
109
110/**
111 * Returns the number of handlers
112 *
113 * @public
114 * @memberof Chain
115 * @instance
116 * @returns {Number} number of handlers in the stack
117 */
118Chain.prototype.count = function count() {
119 return this._stack.length;
120};
121
122/**
123 * Handle server requests, punting them down
124 * the middleware stack.
125 *
126 * @public
127 * @memberof Chain
128 * @instance
129 * @param {Request} req - request
130 * @param {Response} res - response
131 * @param {Function} done - final handler
132 * @returns {undefined} no return value
133 */
134Chain.prototype.run = function run(req, res, done) {
135 var self = this;
136 var index = 0;
137
138 function next(err) {
139 // next callback
140 var handler = self._stack[index++];
141
142 // all done or request closed
143 if (!handler || req.connectionState() === 'close') {
144 process.nextTick(function nextTick() {
145 return done(err, req, res);
146 });
147 return;
148 }
149
150 // call the handler
151 call(handler, err, req, res, self.onceNext ? self._once(next) : next);
152 }
153
154 next();
155 return;
156};
157
158/**
159 * Helper functions
160 * @private
161 */
162
163/**
164 * Invoke a handler.
165 *
166 * @private
167 * @param {Function} handler - handler function
168 * @param {Error|false|*} err - error, abort when true value or false
169 * @param {Request} req - request
170 * @param {Response} res - response
171 * @param {Function} _next - next handler
172 * @returns {undefined} no return value
173 */
174function call(handler, err, req, res, _next) {
175 var arity = handler.length;
176 var hasError = err === false || Boolean(err);
177
178 // Meassure handler timings
179 // _name is assigned in the server and router
180 req._currentHandler = handler._name;
181 req.startHandlerTimer(handler._name);
182
183 function next(nextErr) {
184 req.endHandlerTimer(handler._name);
185 _next(nextErr, req, res);
186 }
187
188 function resolve(value) {
189 if (value && req.log) {
190 // logs resolved value
191 req.log.warn(
192 { value },
193 'Discarded returned value from async handler'
194 );
195 }
196
197 return next();
198 }
199
200 function reject(error) {
201 if (!(error instanceof Error)) {
202 error = new customErrorTypes.AsyncError(
203 {
204 info: {
205 cause: error,
206 handler: handler._name,
207 method: req.method,
208 path: req.path ? req.path() : undefined
209 }
210 },
211 'Async middleware rejected without an error'
212 );
213 }
214 return next(error);
215 }
216
217 if (hasError && arity === 4) {
218 // error-handling middleware
219 handler(err, req, res, next);
220 return;
221 } else if (!hasError && arity < 4) {
222 // request-handling middleware
223 process.nextTick(function nextTick() {
224 const result = handler(req, res, next);
225 if (result && typeof result.then === 'function') {
226 result.then(resolve, reject);
227 }
228 });
229 return;
230 }
231
232 // continue
233 next(err);
234}