// Copyright 2012 Mark Cavage, Inc. All rights reserved.
|
|
'use strict';
|
|
var EventEmitter = require('events').EventEmitter;
|
var http = require('http');
|
var https = require('https');
|
var util = require('util');
|
|
var _ = require('lodash');
|
var assert = require('assert-plus');
|
var errors = require('restify-errors');
|
var mime = require('mime');
|
var spdy = require('spdy');
|
var vasync = require('vasync');
|
|
var Chain = require('./chain');
|
var dtrace = require('./dtrace');
|
var formatters = require('./formatters');
|
var shallowCopy = require('./utils').shallowCopy;
|
var upgrade = require('./upgrade');
|
var deprecationWarnings = require('./deprecationWarnings');
|
var customErrorTypes = require('./errorTypes');
|
|
// Ensure these are loaded
|
var patchRequest = require('./request');
|
var patchResponse = require('./response');
|
|
var domain;
|
var http2;
|
|
patchResponse(http.ServerResponse);
|
patchRequest(http.IncomingMessage);
|
|
///--- Globals
|
|
var sprintf = util.format;
|
|
///--- API
|
|
/**
|
* Creates a new Server.
|
*
|
* @public
|
* @class
|
* @param {Object} options - an options object
|
* @param {String} options.name - Name of the server.
|
* @param {Boolean} [options.dtrace=false] - enable DTrace support
|
* @param {Router} options.router - Router
|
* @param {Object} options.log - [bunyan](https://github.com/trentm/node-bunyan)
|
* instance.
|
* @param {String} [options.url] - Once listen() is called, this will be filled
|
* in with where the server is running.
|
* @param {String|Buffer} [options.certificate] - If you want to create an HTTPS
|
* server, pass in a PEM-encoded certificate and key.
|
* @param {String|Buffer} [options.key] - If you want to create an HTTPS server,
|
* pass in a PEM-encoded certificate and key.
|
* @param {Object} [options.formatters] - Custom response formatters for
|
* `res.send()`.
|
* @param {Boolean} [options.handleUncaughtExceptions=false] - When true restify
|
* will use a domain to catch and respond to any uncaught
|
* exceptions that occur in it's handler stack.
|
* Comes with significant negative performance impact.
|
* [bunyan](https://github.com/trentm/node-bunyan) instance.
|
* response header, default is `restify`. Pass empty string to unset the header.
|
* @param {Object} [options.spdy] - Any options accepted by
|
* [node-spdy](https://github.com/indutny/node-spdy).
|
* @param {Object} [options.http2] - Any options accepted by
|
* [http2.createSecureServer](https://nodejs.org/api/http2.html).
|
* @param {Boolean} [options.handleUpgrades=false] - Hook the `upgrade` event
|
* from the node HTTP server, pushing `Connection: Upgrade` requests through the
|
* regular request handling chain.
|
* @param {Boolean} [options.onceNext=false] - Prevents calling next multiple
|
* times
|
* @param {Boolean} [options.strictNext=false] - Throws error when next() is
|
* called more than once, enabled onceNext option
|
* @param {Object} [options.httpsServerOptions] - Any options accepted by
|
* [node-https Server](http://nodejs.org/api/https.html#https_https).
|
* If provided the following restify server options will be ignored:
|
* spdy, ca, certificate, key, passphrase, rejectUnauthorized, requestCert and
|
* ciphers; however these can all be specified on httpsServerOptions.
|
* @param {Boolean} [options.noWriteContinue=false] - prevents
|
* `res.writeContinue()` in `server.on('checkContinue')` when proxing
|
* @param {Boolean} [options.ignoreTrailingSlash=false] - ignore trailing slash
|
* on paths
|
* @param {Boolean} [options.strictFormatters=true] - enables strict formatters
|
* behavior: a formatter matching the response's content-type is required. If
|
* not found, the response's content-type is automatically set to
|
* 'application/octet-stream'. If a formatter for that content-type is not
|
* found, sending the response errors.
|
* @example
|
* var restify = require('restify');
|
* var server = restify.createServer();
|
*
|
* server.listen(8080, function () {
|
* console.log('ready on %s', server.url);
|
* });
|
*/
|
function Server(options) {
|
assert.object(options, 'options');
|
assert.object(options.log, 'options.log');
|
assert.object(options.router, 'options.router');
|
assert.string(options.name, 'options.name');
|
assert.optionalBool(
|
options.handleUncaughtExceptions,
|
'options.handleUncaughtExceptions'
|
);
|
assert.optionalBool(options.dtrace, 'options.dtrace');
|
assert.optionalBool(options.socketio, 'options.socketio');
|
assert.optionalBool(options.onceNext, 'options.onceNext');
|
assert.optionalBool(options.strictNext, 'options.strictNext');
|
assert.optionalBool(options.strictFormatters, 'options.strictFormatters');
|
|
var self = this;
|
|
EventEmitter.call(this);
|
|
this.onceNext = !!options.onceNext;
|
this.strictNext = !!options.strictNext;
|
this.firstChain = [];
|
this.preChain = new Chain({
|
onceNext: this.onceNext,
|
strictNext: this.strictNext
|
});
|
this.useChain = new Chain({
|
onceNext: this.onceNext,
|
strictNext: this.strictNext
|
});
|
this.log = options.log;
|
this.name = options.name;
|
this.handleUncaughtExceptions = options.handleUncaughtExceptions || false;
|
this.router = options.router;
|
this.secure = false;
|
this.socketio = options.socketio || false;
|
this.dtrace = options.dtrace || false;
|
this._inflightRequests = 0;
|
|
this.strictFormatters = true;
|
if (options.strictFormatters !== undefined) {
|
this.strictFormatters = options.strictFormatters;
|
}
|
|
var fmt = mergeFormatters(options.formatters);
|
this.acceptable = fmt.acceptable;
|
this.formatters = fmt.formatters;
|
this.proxyEvents = [
|
'clientError',
|
'close',
|
'connection',
|
'error',
|
'listening',
|
'secureConnection'
|
];
|
|
if (options.spdy) {
|
this.spdy = true;
|
this.server = spdy.createServer(options.spdy);
|
} else if (options.http2) {
|
// http2 module is not available < v8.4.0 (only with flag <= 8.8.0)
|
// load http2 module here to avoid experimental warning in other cases
|
if (!http2) {
|
try {
|
http2 = require('http2');
|
patchResponse(http2.Http2ServerResponse);
|
patchRequest(http2.Http2ServerRequest);
|
// eslint-disable-next-line no-empty
|
} catch (err) {}
|
}
|
|
assert(
|
http2,
|
'http2 module is not available, ' +
|
'upgrade your Node.js version to >= 8.8.0'
|
);
|
|
this.http2 = true;
|
this.server = http2.createSecureServer(options.http2);
|
} else if ((options.cert || options.certificate) && options.key) {
|
this.ca = options.ca;
|
this.certificate = options.certificate || options.cert;
|
this.key = options.key;
|
this.passphrase = options.passphrase || null;
|
this.secure = true;
|
|
this.server = https.createServer({
|
ca: self.ca,
|
cert: self.certificate,
|
key: self.key,
|
passphrase: self.passphrase,
|
rejectUnauthorized: options.rejectUnauthorized,
|
requestCert: options.requestCert,
|
ciphers: options.ciphers,
|
secureOptions: options.secureOptions
|
});
|
} else if (options.httpsServerOptions) {
|
this.server = https.createServer(options.httpsServerOptions);
|
} else {
|
this.server = http.createServer();
|
}
|
|
this.router.on('mount', this.emit.bind(this, 'mount'));
|
|
if (!options.handleUpgrades) {
|
this.proxyEvents.push('upgrade');
|
}
|
this.proxyEvents.forEach(function forEach(e) {
|
self.server.on(e, self.emit.bind(self, e));
|
});
|
|
// Now the things we can't blindly proxy
|
this.server.on('checkContinue', function onCheckContinue(req, res) {
|
if (self.listeners('checkContinue').length > 0) {
|
self.emit('checkContinue', req, res);
|
return;
|
}
|
|
if (!options.noWriteContinue) {
|
res.writeContinue();
|
}
|
|
self._onRequest(req, res);
|
});
|
|
if (options.handleUpgrades) {
|
this.server.on('upgrade', function onUpgrade(req, socket, head) {
|
req._upgradeRequest = true;
|
var res = upgrade.createResponse(req, socket, head);
|
self._onRequest(req, res);
|
});
|
}
|
|
this.server.on('request', this._onRequest.bind(this));
|
|
this.__defineGetter__('maxHeadersCount', function getMaxHeadersCount() {
|
return self.server.maxHeadersCount;
|
});
|
|
this.__defineSetter__('maxHeadersCount', function setMaxHeadersCount(c) {
|
self.server.maxHeadersCount = c;
|
return c;
|
});
|
|
this.__defineGetter__('url', function getUrl() {
|
if (self.socketPath) {
|
return 'http://' + self.socketPath;
|
}
|
|
var addr = self.address();
|
var str = '';
|
|
if (self.spdy) {
|
str += 'spdy://';
|
} else if (self.secure) {
|
str += 'https://';
|
} else {
|
str += 'http://';
|
}
|
|
if (addr) {
|
str +=
|
addr.family === 'IPv6'
|
? '[' + addr.address + ']'
|
: addr.address;
|
str += ':';
|
str += addr.port;
|
} else {
|
str += '169.254.0.1:0000';
|
}
|
|
return str;
|
});
|
|
// print deprecation messages based on server configuration
|
deprecationWarnings(self);
|
}
|
util.inherits(Server, EventEmitter);
|
|
module.exports = Server;
|
|
///--- Server lifecycle methods
|
|
// eslint-disable-next-line jsdoc/check-param-names
|
/**
|
* Gets the server up and listening.
|
* Wraps node's
|
* [listen()](
|
* http://nodejs.org/docs/latest/api/net.html#net_server_listen_path_callback).
|
*
|
* @public
|
* @memberof Server
|
* @instance
|
* @function listen
|
* @throws {TypeError}
|
* @param {Number} port - Port
|
* @param {Number} [host] - Host
|
* @param {Function} [callback] - optionally get notified when listening.
|
* @returns {undefined} no return value
|
* @example
|
* <caption>You can call like:</caption>
|
* server.listen(80)
|
* server.listen(80, '127.0.0.1')
|
* server.listen('/tmp/server.sock')
|
*/
|
Server.prototype.listen = function listen() {
|
var args = Array.prototype.slice.call(arguments);
|
return this.server.listen.apply(this.server, args);
|
};
|
|
/**
|
* Shuts down this server, and invokes callback (optionally) when done.
|
* Wraps node's
|
* [close()](http://nodejs.org/docs/latest/api/net.html#net_event_close).
|
*
|
* @public
|
* @memberof Server
|
* @instance
|
* @function close
|
* @param {Function} [callback] - callback to invoke when done
|
* @returns {undefined} no return value
|
*/
|
Server.prototype.close = function close(callback) {
|
if (callback) {
|
assert.func(callback, 'callback');
|
}
|
|
this.server.once('close', function onClose() {
|
return callback ? callback() : false;
|
});
|
|
return this.server.close();
|
};
|
|
///--- Routing methods
|
|
/**
|
* Server method opts
|
* @typedef {String|Regexp |Object} Server~methodOpts
|
* @type {Object}
|
* @property {String} name a name for the route
|
* @property {String} path can be any String accepted by
|
* [find-my-way](https://github.com/delvedor/find-my-way)
|
* @example
|
* // a static route
|
* server.get('/foo', function(req, res, next) {});
|
* // a parameterized route
|
* server.get('/foo/:bar', function(req, res, next) {});
|
* // a regular expression
|
* server.get('/example/:file(^\\d+).png', function(req, res, next) {});
|
* // an options object
|
* server.get({
|
* path: '/foo',
|
* }, function(req, res, next) {});
|
*/
|
|
/**
|
* Mounts a chain on the given path against this HTTP verb
|
*
|
* @public
|
* @memberof Server
|
* @instance
|
* @function get
|
* @param {Server~methodOpts} opts - if string, the URL to handle.
|
* if options, the URL to handle, at minimum.
|
* @returns {Route} the newly created route.
|
* @example
|
* server.get('/', function (req, res, next) {
|
* res.send({ hello: 'world' });
|
* next();
|
* });
|
*/
|
Server.prototype.get = serverMethodFactory('GET');
|
|
/**
|
* Mounts a chain on the given path against this HTTP verb
|
*
|
* @public
|
* @memberof Server
|
* @instance
|
* @function head
|
* @param {Server~methodOpts} opts - if string, the URL to handle.
|
* if options, the URL to handle, at minimum.
|
* @returns {Route} the newly created route.
|
*/
|
Server.prototype.head = serverMethodFactory('HEAD');
|
|
/**
|
* Mounts a chain on the given path against this HTTP verb
|
*
|
* @public
|
* @memberof Server
|
* @instance
|
* @function post
|
* @param {Server~methodOpts} post - if string, the URL to handle.
|
* if options, the URL to handle, at minimum.
|
* @returns {Route} the newly created route.
|
*/
|
Server.prototype.post = serverMethodFactory('POST');
|
|
/**
|
* Mounts a chain on the given path against this HTTP verb
|
*
|
* @public
|
* @memberof Server
|
* @instance
|
* @function put
|
* @param {Server~methodOpts} put - if string, the URL to handle.
|
* if options, the URL to handle, at minimum.
|
* @returns {Route} the newly created route.
|
*/
|
Server.prototype.put = serverMethodFactory('PUT');
|
|
/**
|
* Mounts a chain on the given path against this HTTP verb
|
*
|
* @public
|
* @memberof Server
|
* @instance
|
* @function patch
|
* @param {Server~methodOpts} patch - if string, the URL to handle.
|
* if options, the URL to handle, at minimum.
|
* @returns {Route} the newly created route.
|
*/
|
Server.prototype.patch = serverMethodFactory('PATCH');
|
|
/**
|
* Mounts a chain on the given path against this HTTP verb
|
*
|
* @public
|
* @memberof Server
|
* @instance
|
* @function del
|
* @param {Server~methodOpts} opts - if string, the URL to handle.
|
* if options, the URL to handle, at minimum.
|
* @returns {Route} the newly created route.
|
*/
|
Server.prototype.del = serverMethodFactory('DELETE');
|
|
/**
|
* Mounts a chain on the given path against this HTTP verb
|
*
|
* @public
|
* @memberof Server
|
* @instance
|
* @function opts
|
* @param {Server~methodOpts} opts - if string, the URL to handle.
|
* if options, the URL to handle, at minimum.
|
* @returns {Route} the newly created route.
|
*/
|
Server.prototype.opts = serverMethodFactory('OPTIONS');
|
|
///--- Request lifecycle and middleware methods
|
|
// eslint-disable-next-line jsdoc/check-param-names
|
/**
|
* Gives you hooks to run _before_ any routes are located. This gives you
|
* a chance to intercept the request and change headers, etc., that routing
|
* depends on. Note that req.params will _not_ be set yet.
|
*
|
* @public
|
* @memberof Server
|
* @instance
|
* @function pre
|
* @param {...Function|Array} handler - Allows you to add handlers that
|
* run for all routes. *before* routing occurs.
|
* This gives you a hook to change request headers and the like if you need to.
|
* Note that `req.params` will be undefined, as that's filled in *after*
|
* routing.
|
* Takes a function, or an array of functions.
|
* variable number of nested arrays of handler functions
|
* @returns {Object} returns self
|
* @example
|
* server.pre(function(req, res, next) {
|
* req.headers.accept = 'application/json';
|
* return next();
|
* });
|
* @example
|
* <caption>For example, `pre()` can be used to deduplicate slashes in
|
* URLs</caption>
|
* server.pre(restify.pre.dedupeSlashes());
|
*/
|
Server.prototype.pre = function pre() {
|
var self = this;
|
var handlers = Array.prototype.slice.call(arguments);
|
|
argumentsToChain(handlers).forEach(function forEach(handler) {
|
handler._name = handler.name || 'pre-' + self.preChain.count();
|
self.preChain.add(handler);
|
});
|
|
return this;
|
};
|
|
// eslint-disable-next-line jsdoc/check-param-names
|
/**
|
* Gives you hooks that run before restify touches a request. These hooks
|
* allow you to do processing early in the request/response life cycle without
|
* the overhead of the restify framework. You can not yield the event loop in
|
* this handler.
|
*
|
* The function handler accepts two parameters: req, res. If you want restify
|
* to ignore this request, return false from your handler. Return true or
|
* undefined to let restify continue handling the request.
|
*
|
* When false is returned, restify immediately stops handling the request. This
|
* means that no further middleware will be executed for any chain and routing
|
* will not occure. All request/response handling for an incoming request must
|
* be done inside the first handler if you intend to return false. This
|
* includes things like closing the response and returning a status code.
|
*
|
* The only work restify does for a first handler is to increment the number of
|
* inflightRequests prior to calling the chain, and decrement that value if the
|
* handler returns false. Returning anything other than true, false, undefined,
|
* or null will cause an exception to be thrown.
|
*
|
* Since server.first is designed to bypass the restify framework, there are
|
* naturally trade-offs you make when using this API:
|
* * Standard restify lifecycle events such as 'after' are not triggered for
|
* any request that you return false from a handler for
|
* * Invoking any of the restify req/res APIs from within a first handler is
|
* unspecified behavior, as the restify framework hasn't built up state for
|
* the request yet.
|
* * There are no request timers available at the time that the first chain
|
* runs.
|
* * And more! Beware doing anything with restify in these handlers. They are
|
* designed to give you similar access to the req/res as you would have if
|
* you were directly using node.js' http module, they are outside of the
|
* restify framework!
|
*
|
* @public
|
* @memberof Server
|
* @instance
|
* @function first
|
* @param {...Function} handler - Allows you to add handlers that
|
* run for all requests, *before* restify touches the request.
|
* This gives you a hook to change request headers and the like if you need to.
|
* Note that `req.params` will be undefined, as that's filled in *after*
|
* routing.
|
|
* Takes one or more functions.
|
* @returns {Object} returns self
|
* @example
|
* server.first(function(req, res) {
|
* if(server.inflightRequests() > 100) {
|
* res.statusCode = 503;
|
* res.end();
|
* return false
|
* }
|
* return true;
|
* })
|
*/
|
Server.prototype.first = function first() {
|
var args = Array.prototype.slice.call(arguments);
|
for (var i = 0; i < args.length; i++) {
|
assert.func(args[i]);
|
this.firstChain.push(args[i]);
|
}
|
return this;
|
};
|
|
// eslint-disable-next-line jsdoc/check-param-names
|
/**
|
* Allows you to add in handlers that run for all routes. Note that handlers
|
* added
|
* via `use()` will run only after the router has found a matching route. If no
|
* match is found, these handlers will never run. Takes a function, or an array
|
* of functions.
|
*
|
* You can pass in any combination of functions or array of functions.
|
*
|
* @public
|
* @memberof Server
|
* @instance
|
* @function use
|
* @param {...Function|Array} handler - A variable number of handler functions
|
* * and/or a
|
* variable number of nested arrays of handler functions
|
* @returns {Object} returns self
|
*/
|
Server.prototype.use = function use() {
|
var self = this;
|
var handlers = Array.prototype.slice.call(arguments);
|
|
argumentsToChain(handlers).forEach(function forEach(handler) {
|
handler._name = handler.name || 'use-' + self.useChain.count();
|
self.useChain.add(handler);
|
});
|
|
return this;
|
};
|
|
/**
|
* Minimal port of the functionality offered by Express.js Route Param
|
* Pre-conditions
|
*
|
* This basically piggy-backs on the `server.use` method. It attaches a
|
* new middleware function that only fires if the specified parameter exists
|
* in req.params
|
*
|
* Exposes an API:
|
* server.param("user", function (req, res, next) {
|
* // load the user's information here, always making sure to call next()
|
* });
|
*
|
* @see http://expressjs.com/guide.html#route-param%20pre-conditions
|
* @public
|
* @memberof Server
|
* @instance
|
* @function param
|
* @param {String} name - The name of the URL param to respond to
|
* @param {Function} fn - The middleware function to execute
|
* @returns {Object} returns self
|
*/
|
Server.prototype.param = function param(name, fn) {
|
this.use(function _param(req, res, next) {
|
if (req.params && req.params.hasOwnProperty(name)) {
|
fn.call(this, req, res, next, req.params[name], name);
|
} else {
|
next();
|
}
|
});
|
|
return this;
|
};
|
|
/**
|
* Removes a route from the server.
|
* You pass in the route 'blob' you got from a mount call.
|
*
|
* @public
|
* @memberof Server
|
* @instance
|
* @function rm
|
* @throws {TypeError} on bad input.
|
* @param {String} routeName - the route name.
|
* @returns {Boolean} true if route was removed, false if not.
|
*/
|
Server.prototype.rm = function rm(routeName) {
|
var route = this.router.unmount(routeName);
|
return !!route;
|
};
|
|
///--- Info and debug methods
|
|
/**
|
* Returns the server address.
|
* Wraps node's
|
* [address()](http://nodejs.org/docs/latest/api/net.html#net_server_address).
|
*
|
* @public
|
* @memberof Server
|
* @instance
|
* @function address
|
* @returns {Object} Address of server
|
* @example
|
* server.address()
|
* @example
|
* <caption>Output:</caption>
|
* { address: '::', family: 'IPv6', port: 8080 }
|
*/
|
Server.prototype.address = function address() {
|
return this.server.address();
|
};
|
|
/**
|
* Returns the number of inflight requests currently being handled by the server
|
*
|
* @public
|
* @memberof Server
|
* @instance
|
* @function inflightRequests
|
* @returns {number} number of inflight requests
|
*/
|
Server.prototype.inflightRequests = function inflightRequests() {
|
var self = this;
|
return self._inflightRequests;
|
};
|
|
/**
|
* Return debug information about the server.
|
*
|
* @public
|
* @memberof Server
|
* @instance
|
* @function debugInfo
|
* @returns {Object} debug info
|
* @example
|
* server.getDebugInfo()
|
* @example
|
* <caption>Output:</caption>
|
* {
|
* routes: [
|
* {
|
* name: 'get',
|
* method: 'get',
|
* input: '/',
|
* compiledRegex: /^[\/]*$/,
|
* compiledUrlParams: null,
|
* handlers: [Array]
|
* }
|
* ],
|
* server: {
|
* formatters: {
|
* 'application/javascript': [Function: formatJSONP],
|
* 'application/json': [Function: formatJSON],
|
* 'text/plain': [Function: formatText],
|
* 'application/octet-stream': [Function: formatBinary]
|
* },
|
* address: '::',
|
* port: 8080,
|
* inflightRequests: 0,
|
* pre: [],
|
* use: [ 'parseQueryString', '_jsonp' ],
|
* after: []
|
* }
|
* }
|
*/
|
Server.prototype.getDebugInfo = function getDebugInfo() {
|
var self = this;
|
|
// map an array of function to an array of function names
|
var funcNameMapper = function funcNameMapper(handler) {
|
if (handler.name === '') {
|
return 'anonymous';
|
} else {
|
return handler.name;
|
}
|
};
|
|
if (!self._debugInfo) {
|
var addressInfo = self.server.address();
|
|
// output the actual routes registered with restify
|
var routeInfo = self.router.getDebugInfo();
|
|
var preHandlers = self.preChain.getHandlers().map(funcNameMapper);
|
var useHandlers = self.useChain.getHandlers().map(funcNameMapper);
|
|
// get each route's handler chain
|
var routes = _.map(routeInfo, function mapValues(route) {
|
route.handlers = Array.prototype.concat.call(
|
// TODO: should it contain use handlers?
|
useHandlers,
|
route.handlers.map(funcNameMapper)
|
);
|
return route;
|
});
|
|
self._debugInfo = {
|
routes: routes,
|
server: {
|
formatters: self.formatters,
|
// if server is not yet listening, addressInfo may be null
|
address: addressInfo && addressInfo.address,
|
port: addressInfo && addressInfo.port,
|
inflightRequests: self.inflightRequests(),
|
pre: preHandlers,
|
use: useHandlers,
|
after: self.listeners('after').map(funcNameMapper)
|
}
|
};
|
}
|
|
return self._debugInfo;
|
};
|
|
/**
|
* toString() the server for easy reading/output.
|
*
|
* @public
|
* @memberof Server
|
* @instance
|
* @function toString
|
* @returns {String} stringified server
|
* @example
|
* server.toString()
|
* @example
|
* <caption>Output:</caption>
|
* Accepts: application/json, text/plain, application/octet-stream,
|
* application/javascript
|
* Name: restify
|
* Pre: []
|
* Router: RestifyRouter:
|
* DELETE: []
|
* GET: [get]
|
* HEAD: []
|
* OPTIONS: []
|
* PATCH: []
|
* POST: []
|
* PUT: []
|
*
|
* Routes:
|
* get: [parseQueryString, _jsonp, function]
|
* Secure: false
|
* Url: http://[::]:8080
|
* Version:
|
*/
|
Server.prototype.toString = function toString() {
|
var LINE_FMT = '\t%s: %s\n';
|
var SUB_LINE_FMT = '\t\t%s: %s\n';
|
var str = '';
|
|
function handlersToString(arr) {
|
var s =
|
'[' +
|
arr
|
.map(function map(b) {
|
return b.name || 'function';
|
})
|
.join(', ') +
|
']';
|
|
return s;
|
}
|
|
str += sprintf(LINE_FMT, 'Accepts', this.acceptable.join(', '));
|
str += sprintf(LINE_FMT, 'Name', this.name);
|
str += sprintf(
|
LINE_FMT,
|
'Pre',
|
handlersToString(this.preChain.getHandlers())
|
);
|
str += sprintf(LINE_FMT, 'Router', '');
|
this.router
|
.toString()
|
.split('\n')
|
.forEach(function forEach(line) {
|
str += sprintf('\t\t%s\n', line);
|
});
|
str += sprintf(LINE_FMT, 'Routes', '');
|
_.forEach(this.router.getRoutes(), function forEach(route, routeName) {
|
var handlers = handlersToString(route.chain.getHandlers());
|
str += sprintf(SUB_LINE_FMT, routeName, handlers);
|
});
|
str += sprintf(LINE_FMT, 'Secure', this.secure);
|
str += sprintf(LINE_FMT, 'Url', this.url);
|
|
return str;
|
};
|
|
///--- Private methods
|
|
// Lifecycle:
|
//
|
// 1. _onRequest (handle new request, setup request and triggers pre)
|
// 2. _runPre
|
// 3. _afterPre (runs after pre handlers are finisehd, triggers route)
|
// 4. _runRoute (route lookup)
|
// 5. _runUse (runs use handlers, if route exists)
|
// 6. Runs route handlers
|
// 7. _afterRoute (runs after route handlers are finised,
|
// triggers use)
|
// 8. _finishReqResCycle (on response "finish" and "error" events)
|
//
|
// Events:
|
// e.1 after (triggered when response is flushed)
|
//
|
// Errors:
|
// e.1 _onHandlerError (runs when next was called with an Error)
|
// e.2 _routeErrorResponse
|
// e.1 _onHandlerError (when, next('string') called, trigger route by name)
|
// e.2 _afterRoute
|
|
/**
|
* Setup request and calls _onRequest to run middlewares and call router
|
*
|
* @private
|
* @memberof Server
|
* @instance
|
* @function _onRequest
|
* @param {Object} req - the request object
|
* @param {Object} res - the response object
|
* @returns {undefined} no return value
|
* @fires Request,Response#request
|
*/
|
Server.prototype._onRequest = function _onRequest(req, res) {
|
var self = this;
|
|
// Increment the number of inflight requests prior to calling the firstChain
|
// handlers. This accomplishes two things. First, it gives earliest an
|
// accurate count of how many inflight requests there would be including
|
// this new request. Second, it intentionally winds up the inflight request
|
// accounting with the implementation of firstChain. Note how we increment
|
// here, but decrement down inside the for loop below. It's easy to end up
|
// with race conditions betwen inflight request accounting and inflight
|
// request load shedding, causing load shedding to reject/allow too many
|
// requests. The current implementation of firstChain is designed to
|
// remove those race conditions. By winding these implementations up with
|
// one another, it makes it clear that moving around the inflight request
|
// accounting will change the behavior of earliest.
|
self._inflightRequests++;
|
|
// Give the first chain the earliest possible opportunity to process
|
// this request before we do any work on it.
|
var firstChain = self.firstChain;
|
for (var i = 0; i < firstChain.length; i++) {
|
var handle = firstChain[i](req, res);
|
// Limit the range of values we will accept as return results of
|
// first handlers. This helps us maintain forward compatibility by
|
// ensuring users don't rely on undocumented/unspecified behavior.
|
assert.ok(
|
handle === true ||
|
handle === false ||
|
handle === undefined ||
|
handle === null,
|
'Return value of first[' +
|
i +
|
'] must be: ' +
|
'boolean, undefined, or null'
|
);
|
// If the first handler returns false, stop handling the request
|
// immediately.
|
if (handle === false) {
|
self._inflightRequests--;
|
return;
|
}
|
}
|
|
this.emit('request', req, res);
|
|
// Skip Socket.io endpoints
|
if (this.socketio && /^\/socket\.io.*/.test(req.url)) {
|
self._inflightRequests--;
|
return;
|
}
|
|
// Decorate req and res objects
|
self._setupRequest(req, res);
|
|
// Run in domain to catch async errors
|
// It has significant negative performance impact
|
// Warning: this feature depends on the deprecated domains module
|
if (self.handleUncaughtExceptions) {
|
// In Node v12.x requiring the domain module has a negative performance
|
// impact. As using domains in restify is optional and only required
|
// with the `handleUncaughtExceptions` options, we apply a singleton
|
// pattern to avoid any performance regression in the default scenario.
|
if (!domain) {
|
domain = require('domain');
|
}
|
|
var handlerDomain = domain.create();
|
handlerDomain.add(req);
|
handlerDomain.add(res);
|
handlerDomain.on('error', function onError(err) {
|
self._onHandlerError(err, req, res, true);
|
});
|
handlerDomain.run(function run() {
|
self._runPre(req, res);
|
});
|
} else {
|
self._runPre(req, res);
|
}
|
};
|
|
/**
|
* Run pre handlers
|
*
|
* @private
|
* @memberof Server
|
* @instance
|
* @function _runPre
|
* @param {Object} req - the request object
|
* @param {Object} res - the response object
|
* @returns {undefined} no return value
|
* @fires Request,Response#request
|
*/
|
Server.prototype._runPre = function _runPre(req, res) {
|
var self = this;
|
|
// emit 'pre' event before we run the pre handlers
|
self.emit('pre', req, res);
|
|
// Run "pre"
|
req._currentHandler = 'pre';
|
req._timePreStart = process.hrtime();
|
|
self.preChain.run(req, res, function preChainDone(err) {
|
// Execution time of a handler with error can be significantly lower
|
req._timePreEnd = process.hrtime();
|
self._afterPre(err, req, res);
|
});
|
};
|
|
/**
|
* After pre handlers finished
|
*
|
* @private
|
* @memberof Server
|
* @instance
|
* @function _afterPre
|
* @param {Error|false|undefined} err - pre handler error
|
* @param {Request} req - request
|
* @param {Response} res - response
|
* @returns {undefined} no return value
|
*/
|
Server.prototype._afterPre = function _afterPre(err, req, res) {
|
var self = this;
|
|
// Handle error
|
if (err) {
|
self._onHandlerError(err, req, res);
|
self._finishReqResCycle(req, res, err);
|
return;
|
}
|
|
// Stop
|
if (err === false) {
|
self._onHandlerStop(req, res);
|
return;
|
}
|
|
self._runRoute(req, res);
|
};
|
|
/**
|
* Find route and run handlers
|
*
|
* @private
|
* @memberof Server
|
* @instance
|
* @function _runRoute
|
* @param {Object} req - the request object
|
* @param {Object} res - the response object
|
* @returns {undefined} no return value
|
* @fires Request,Response#request
|
*/
|
Server.prototype._runRoute = function _runRoute(req, res) {
|
var self = this;
|
|
var routeHandler = self.router.lookup(req, res);
|
|
if (!routeHandler) {
|
self.router.defaultRoute(req, res, function afterRouter(err) {
|
self._afterRoute(err, req, res);
|
});
|
return;
|
}
|
|
// Emit routed
|
self.emit('routed', req, res, req.route);
|
|
self._runUse(req, res, function afterUse() {
|
// DTrace
|
if (self.dtrace) {
|
dtrace._rstfy_probes['route-start'].fire(function fire() {
|
return [
|
self.name,
|
req.route.name,
|
req._dtraceId,
|
req.method,
|
req.href(),
|
req.headers
|
];
|
});
|
}
|
|
req._timeRouteStart = process.hrtime();
|
routeHandler(req, res, function afterRouter(err) {
|
// Execution time of a handler with error can be significantly lower
|
req._timeRouteEnd = process.hrtime();
|
|
// DTrace
|
if (self.dtrace) {
|
dtrace._rstfy_probes['route-done'].fire(function fire() {
|
return [
|
self.name,
|
req.route.name,
|
req._dtraceId,
|
res.statusCode || 200,
|
res.headers
|
];
|
});
|
}
|
|
self._afterRoute(err, req, res);
|
});
|
});
|
};
|
|
/**
|
* After use handlers finished
|
*
|
* @private
|
* @memberof Server
|
* @instance
|
* @function _afterRoute
|
* @param {Error|false|undefined} err - use handler error
|
* @param {Request} req - request
|
* @param {Response} res - response
|
* @returns {undefined} no return value
|
*/
|
Server.prototype._afterRoute = function _afterRoute(err, req, res) {
|
var self = this;
|
|
res._handlersFinished = true;
|
|
// Handle error
|
if (err) {
|
self._onHandlerError(err, req, res);
|
self._finishReqResCycle(req, res, err);
|
return;
|
}
|
|
// Trigger finish
|
self._finishReqResCycle(req, res, err);
|
};
|
|
/**
|
* Run use handlers
|
*
|
* @private
|
* @memberof Server
|
* @instance
|
* @function _runUse
|
* @param {Object} req - the request object
|
* @param {Object} res - the response object
|
* @param {Function} next - next
|
* @returns {undefined} no return value
|
* @fires Request,Response#request
|
*/
|
Server.prototype._runUse = function _runUse(req, res, next) {
|
var self = this;
|
|
// Run "use"
|
req._currentHandler = 'use';
|
req._timeUseStart = process.hrtime();
|
|
self.useChain.run(req, res, function useChainDone(err) {
|
// Execution time of a handler with error can be significantly lower
|
req._timeUseEnd = process.hrtime();
|
self._afterUse(err, req, res, next);
|
});
|
};
|
|
/**
|
* After use handlers finished
|
*
|
* @private
|
* @memberof Server
|
* @instance
|
* @function _afterUse
|
* @param {Error|false|undefined} err - use handler error
|
* @param {Request} req - request
|
* @param {Response} res - response
|
* @param {Function} next - next
|
* @returns {undefined} no return value
|
*/
|
Server.prototype._afterUse = function _afterUse(err, req, res, next) {
|
var self = this;
|
|
// Handle error
|
if (err) {
|
self._onHandlerError(err, req, res);
|
self._finishReqResCycle(req, res, err);
|
return;
|
}
|
|
// Stop
|
if (err === false) {
|
self._onHandlerStop(req, res);
|
return;
|
}
|
|
next();
|
};
|
|
/**
|
* Runs after next(false) is called
|
*
|
* @private
|
* @memberof Server
|
* @instance
|
* @function _onHandlerStop
|
* @param {Request} req - request
|
* @param {Response} res - response
|
* @returns {undefined} no return value
|
*/
|
Server.prototype._onHandlerStop = function _onHandlerStop(req, res) {
|
res._handlersFinished = true;
|
this._finishReqResCycle(req, res);
|
};
|
|
/**
|
* After route handlers finished
|
* NOTE: only called when last handler calls next([err])
|
*
|
* @private
|
* @memberof Server
|
* @instance
|
* @function _onHandlerError
|
* @param {Error|String|undefined} err - router handler error or route name
|
* @param {Request} req - request
|
* @param {Response} res - response
|
* @param {boolean} isUncaught - whether the error is uncaught
|
* @returns {undefined} no return value
|
*/
|
Server.prototype._onHandlerError = function _onHandlerError(
|
err,
|
req,
|
res,
|
isUncaught
|
) {
|
var self = this;
|
|
// Call route by name
|
if (!isUncaught && _.isString(err)) {
|
var routeName = err;
|
var routeHandler = self.router.lookupByName(routeName, req, res);
|
|
// Cannot find route by name, called when next('route-name') doesn't
|
// find any route, it's a 5xx error as it's a programatic error
|
if (!routeHandler) {
|
var routeByNameErr = new customErrorTypes.RouteMissingError(
|
"Route by name doesn't exist"
|
);
|
routeByNameErr.code = 'ENOEXIST';
|
self._afterRoute(routeByNameErr, req, res);
|
return;
|
}
|
routeHandler(req, res, function afterRouter(routeErr) {
|
self._afterRoute(routeErr, req, res);
|
});
|
return;
|
}
|
|
// Handlers don't continue when error happen
|
res._handlersFinished = true;
|
|
// Preserve handler err for finish event
|
res.err = res.err || err;
|
|
// Error happened in router handlers
|
self._routeErrorResponse(req, res, err, isUncaught);
|
};
|
|
/**
|
* Set up the request before routing and execution of handler chain functions.
|
*
|
* @private
|
* @memberof Server
|
* @instance
|
* @function _setupRequest
|
* @param {Object} req - the request object
|
* @param {Object} res - the response object
|
* @returns {undefined} no return value
|
*/
|
Server.prototype._setupRequest = function _setupRequest(req, res) {
|
var self = this;
|
|
// Extend request
|
req._dtraceId = dtrace.nextId();
|
req.log = res.log = self.log;
|
req._date = new Date();
|
req._timeStart = process.hrtime();
|
req.serverName = self.name;
|
req.params = {};
|
req.timers = [];
|
req.dtrace = self.dtrace;
|
|
// Extend response
|
res.acceptable = self.acceptable;
|
res.formatters = self.formatters;
|
res.req = req;
|
res.serverName = self.name;
|
res._handlersFinished = false;
|
res._flushed = false;
|
res._strictFormatters = this.strictFormatters;
|
|
// set header only if name isn't empty string
|
if (self.name !== '') {
|
res.setHeader('Server', self.name);
|
}
|
|
// Request lifecycle events
|
|
// attach a listener for 'aborted' events, this will let us set
|
// a flag so that we can stop processing the request if the client aborts
|
// the connection (or we lose the connection).
|
// we consider a closed request as flushed from metrics point of view
|
function onReqAborted() {
|
// Request was aborted, override the status code
|
var err = new customErrorTypes.RequestCloseError();
|
err.statusCode = 444;
|
|
// For backward compatibility we only set connection state to "close"
|
// for RequestCloseError, also aborted is always immediatly followed
|
// by a "close" event.
|
// We don't set _connectionState to "close" in the happy path
|
req._connectionState = 'close';
|
|
// Set status code and err for audit as req is already closed connection
|
res.statusCode = err.statusCode;
|
res.err = err;
|
}
|
|
// Response lifecycle events
|
function onResFinish() {
|
var processHrTime = process.hrtime();
|
|
res._flushed = true;
|
req._timeFlushed = processHrTime;
|
|
// Response may get flushed before handler callback is triggered
|
req._timeFlushed = processHrTime;
|
req._timePreEnd = req._timePreEnd || processHrTime;
|
req._timeUseEnd = req._timeUseEnd || processHrTime;
|
req._timeRouteEnd = req._timeRouteEnd || processHrTime;
|
|
// In Node < 10 "close" event dont fire always
|
// https://github.com/nodejs/node/pull/20611
|
self._finishReqResCycle(req, res);
|
}
|
|
// We are handling when connection is being closed prematurely outside of
|
// restify. It's not because the req is aborted.
|
function onResClose() {
|
res._flushed = true;
|
|
// Finish may already set the req._timeFlushed
|
req._timeFlushed = req._timeFlushed || process.hrtime();
|
|
self._finishReqResCycle(req, res, res.err);
|
}
|
|
// Request events
|
req.once('aborted', onReqAborted);
|
|
// Response events
|
res.once('finish', onResFinish);
|
res.once('close', onResClose);
|
|
// attach a listener for the response's 'redirect' event
|
res.on('redirect', function onRedirect(redirectLocation) {
|
self.emit('redirect', redirectLocation);
|
});
|
};
|
|
/**
|
* Maintaining the end of the request-response cycle:
|
* - emitting after event
|
* - updating inflight requests metrics
|
* Check if the response is finished, and if not, wait for it before firing the
|
* response object.
|
*
|
* @private
|
* @memberof Server
|
* @instance
|
* @function _finishReqResCycle
|
* @param {Object} req - the request object
|
* @param {Object} res - the response object
|
* @param {Object} [err] - a possible error as a result of failed route matching
|
* or failed execution of the handler array.
|
* @returns {undefined} no return value
|
*/
|
Server.prototype._finishReqResCycle = function _finishReqResCycle(
|
req,
|
res,
|
err
|
) {
|
var self = this;
|
var route = req.route; // can be undefined when 404 or error
|
|
if (res._finished) {
|
return;
|
}
|
|
if (res._flushed && res._handlersFinished) {
|
// decrement number of requests
|
self._inflightRequests--;
|
res._finished = true;
|
req._timeFinished = process.hrtime();
|
|
// after event has signature of function(req, res, route, err) {...}
|
var finalErr = err || res.err;
|
req.emit('restifyDone', route, finalErr);
|
self.emit('after', req, res, route, finalErr);
|
} else {
|
// Store error for when the response is flushed and we actually emit the
|
// 'after' event. The "err" object passed to this method takes
|
// precedence, but in case it's not set, "res.err" may have been already
|
// set by another code path and we want to preserve it. The caveat thus
|
// is that the 'after' event will be emitted with the latest error that
|
// was set before the response is fully flushed. While not ideal, this
|
// is on purpose and accepted as a reasonable trade-off for now.
|
res.err = err || res.err;
|
}
|
};
|
|
/**
|
* Helper function to, when on router error, emit error events and then
|
* flush the err.
|
*
|
* @private
|
* @memberof Server
|
* @instance
|
* @function _routeErrorResponse
|
* @param {Request} req - the request object
|
* @param {Response} res - the response object
|
* @param {Error} err - error
|
* @param {boolean} isUncaught - whether the error is uncaught
|
* @returns {undefined} no return value
|
*/
|
Server.prototype._routeErrorResponse = function _routeErrorResponse(
|
req,
|
res,
|
err,
|
isUncaught
|
) {
|
var self = this;
|
|
if (
|
isUncaught &&
|
self.handleUncaughtExceptions &&
|
self.listenerCount('uncaughtException') > 1
|
) {
|
self.emit(
|
'uncaughtException',
|
req,
|
res,
|
req.route,
|
err,
|
function uncaughtExceptionCompleted() {
|
// We provide a callback to listeners of the 'uncaughtException'
|
// event and we call _finishReqResCycle when that callback is
|
// called so that, in case the actual request/response lifecycle
|
// was completed _before_ the error was thrown or emitted, and
|
// thus _before_ route handlers were marked as "finished", we
|
// can still mark the req/res lifecycle as complete.
|
// This edge case can occur when e.g. a client aborts a request
|
// and the route handler that handles that request throws an
|
// uncaught exception _after_ the request was aborted and the
|
// response was closed.
|
self._finishReqResCycle(req, res, err);
|
}
|
);
|
return;
|
}
|
|
self._emitErrorEvents(req, res, null, err, function emitError() {
|
// Prevent double handling
|
if (res._sent) {
|
return;
|
}
|
|
// only automatically send errors that are known (e.g., restify-errors)
|
if (err instanceof Error && _.isNumber(err.statusCode)) {
|
res.send(err);
|
return;
|
}
|
|
// if the thrown exception is not really an Error object, e.g.,
|
// "throw 'foo';"
|
// try to do best effort here to pass on that value by casting it to a
|
// string. This should work even for falsy values like 0, false, null,
|
// or undefined.
|
res.send(new errors.InternalError(String(err)));
|
});
|
};
|
|
/**
|
* Emit error events when errors are encountered either while attempting to
|
* route the request (via router) or while executing the handler chain.
|
*
|
* @private
|
* @memberof Server
|
* @instance
|
* @function _emitErrorEvents
|
* @param {Object} req - the request object
|
* @param {Object} res - the response object
|
* @param {Object} route - the current route, if applicable
|
* @param {Object} err - an error object
|
* @param {Object} cb - callback function
|
* @returns {undefined} no return value
|
* @fires Error#restifyError
|
*/
|
Server.prototype._emitErrorEvents = function _emitErrorEvents(
|
req,
|
res,
|
route,
|
err,
|
cb
|
) {
|
var self = this;
|
// Error can be of any type: undefined, Error, Number, etc. so we need
|
// to protect ourselves from trying to resolve names from non Error objects
|
var errName = err && err.name;
|
var normalizedErrName = errName && errEvtNameFromError(err);
|
|
req.log.trace(
|
{
|
err: err,
|
errName: normalizedErrName
|
},
|
'entering emitErrorEvents',
|
errName
|
);
|
|
var errEvtNames = [];
|
|
// if we have listeners for the specific error, fire those first.
|
// if there's no error name, we should not emit an event
|
if (normalizedErrName && self.listeners(normalizedErrName).length > 0) {
|
errEvtNames.push(normalizedErrName);
|
}
|
|
// or if we have a generic error listener. always fire generic error event
|
// listener afterwards.
|
if (self.listeners('restifyError').length > 0) {
|
errEvtNames.push('restifyError');
|
}
|
|
// kick off the async listeners
|
return vasync.forEachPipeline(
|
{
|
inputs: errEvtNames,
|
func: function emitError(errEvtName, vasyncCb) {
|
self.emit(errEvtName, req, res, err, function emitErrDone() {
|
// the error listener may return arbitrary objects, throw
|
// them away and continue on. don't want vasync to take
|
// that error and stop, we want to emit every single event.
|
return vasyncCb();
|
});
|
}
|
},
|
// eslint-disable-next-line handle-callback-err
|
function onResult(__, results) {
|
// vasync will never return error here since we throw them away.
|
return cb();
|
}
|
);
|
};
|
|
///--- Helpers
|
|
/**
|
* Verify and flatten a nested array of request handlers.
|
*
|
* @private
|
* @function argumentsToChain
|
* @throws {TypeError}
|
* @param {Function[]} handlers - pass through of funcs from server.[method]
|
* @returns {Array} request handlers
|
*/
|
function argumentsToChain(handlers) {
|
assert.array(handlers, 'handlers');
|
|
var chain = [];
|
|
// A recursive function for unwinding a nested array of handlers into a
|
// single chain.
|
function process(array) {
|
for (var i = 0; i < array.length; i++) {
|
if (Array.isArray(array[i])) {
|
// Recursively call on nested arrays
|
process(array[i]);
|
continue;
|
}
|
// If an element of the array isn't an array, ensure it is a
|
// handler function and then push it onto the chain of handlers
|
assert.func(array[i], 'handler');
|
chain.push(array[i]);
|
}
|
|
return chain;
|
}
|
|
// Return the chain, note that if `handlers` is an empty array, this will
|
// return an empty array.
|
return process(handlers);
|
}
|
|
/**
|
* merge optional formatters with the default formatters to create a single
|
* formatters object. the passed in optional formatters object looks like:
|
* formatters: {
|
* 'application/foo': function formatFoo(req, res, body) {...}
|
* }
|
* @private
|
* @function mergeFormatters
|
* @param {Object} fmt user specified formatters object
|
* @returns {Object}
|
*/
|
|
function mergeFormatters(fmt) {
|
var arr = [];
|
var obj = {};
|
|
function addFormatter(src, k) {
|
assert.func(src[k], 'formatter');
|
|
var q = 1.0; // RFC 2616 sec14 - The default value is q=1
|
var t = k;
|
|
if (k.indexOf(';') !== -1) {
|
var tmp = k.split(/\s*;\s*/);
|
t = tmp[0];
|
|
if (tmp[1].indexOf('q=') !== -1) {
|
q = parseFloat(tmp[1].split('=')[1]);
|
}
|
}
|
|
if (k.indexOf('/') === -1) {
|
k = mime.getType(k);
|
}
|
|
obj[t] = src[k];
|
arr.push({
|
q: q,
|
t: t
|
});
|
}
|
|
Object.keys(formatters).forEach(addFormatter.bind(this, formatters));
|
Object.keys(fmt || {}).forEach(addFormatter.bind(this, fmt || {}));
|
|
arr = arr
|
.sort(function sort(a, b) {
|
return b.q - a.q;
|
})
|
.map(function map(a) {
|
return a.t;
|
});
|
|
return {
|
formatters: obj,
|
acceptable: arr
|
};
|
}
|
|
/**
|
* Map an Error's .name property into the actual event name that is emitted
|
* by the restify server object.
|
*
|
* @function
|
* @private errEvtNameFromError
|
* @param {Object} err - an error object
|
* @returns {String} an event name to emit
|
*/
|
function errEvtNameFromError(err) {
|
if (err.name === 'ResourceNotFoundError') {
|
// remap the name for router errors
|
return 'NotFound';
|
} else if (err.name === 'InvalidVersionError') {
|
// remap the name for router errors
|
return 'VersionNotAllowed';
|
} else if (err.name) {
|
return err.name.replace(/Error$/, '');
|
}
|
// If the err is not an Error, then just return an empty string
|
return '';
|
}
|
|
/**
|
* Mounts a chain on the given path against this HTTP verb
|
*
|
* @private
|
* @function serverMethodFactory
|
* @param {String} method - name of the HTTP method
|
* @returns {Function} factory
|
*/
|
function serverMethodFactory(method) {
|
return function serverMethod(opts) {
|
if (opts instanceof RegExp || typeof opts === 'string') {
|
opts = {
|
path: opts
|
};
|
} else if (typeof opts === 'object') {
|
opts = shallowCopy(opts);
|
} else {
|
throw new TypeError('path (string) required');
|
}
|
|
if (arguments.length < 2) {
|
throw new TypeError('handler (function) required');
|
}
|
|
opts.method = method;
|
opts.path = opts.path || opts.url;
|
|
// We accept both a variable number of handler functions, a
|
// variable number of nested arrays of handler functions, or a mix
|
// of both
|
var handlers = Array.prototype.slice.call(arguments, 1);
|
var chain = argumentsToChain(handlers);
|
var route = this.router.mount(opts, chain);
|
|
return route.name;
|
};
|
}
|