restify
Version:
REST framework
1,609 lines (1,434 loc) • 49 kB
JavaScript
// Copyright 2012 Mark Cavage, Inc. All rights reserved.
'use strict';
var domain = require('domain');
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 once = require('once');
var semver = require('semver');
var spdy = require('spdy');
var uuid = require('uuid');
var vasync = require('vasync');
var dtrace = require('./dtrace');
var formatters = require('./formatters');
var shallowCopy = require('./utils').shallowCopy;
var upgrade = require('./upgrade');
var deprecationWarnings = require('./deprecationWarnings');
// Ensure these are loaded
var patchRequest = require('./request');
var patchResponse = require('./response');
var http2;
patchResponse(http.ServerResponse);
patchRequest(http.IncomingMessage);
///--- Globals
var sprintf = util.format;
var maxSatisfying = semver.maxSatisfying;
var ResourceNotFoundError = errors.ResourceNotFoundError;
var PROXY_EVENTS = [
'clientError',
'close',
'connection',
'error',
'listening',
'secureConnection'
];
///--- API
/**
* Creates a new Server.
*
* @public
* @class
* @param {Object} options - an options object
* @param {String} options.name - Name of the server.
* @param {Router} options.router - Router
* @param {Object} options.log - [bunyan](https://github.com/trentm/node-bunyan)
* instance.
* @param {String|Array} [options.version] - Default version(s) to use in all
* routes.
* @param {String[]} [options.acceptable] - List of content-types this
* server can respond with.
* @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.
* [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 {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.strictRouting=false] - If set, Restify
* will treat "/foo" and "/foo/" as different paths.
* @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');
var self = this;
EventEmitter.call(this);
this.before = [];
this.chain = [];
this.log = options.log;
this.name = options.name;
this.handleUncaughtExceptions = options.handleUncaughtExceptions || false;
this.router = options.router;
this.routes = {};
this.secure = false;
this.socketio = options.socketio || false;
this._once = options.strictNext === false ? once : once.strict;
this.versions = options.versions || options.version || [];
this._inflightRequests = 0;
var fmt = mergeFormatters(options.formatters);
this.acceptable = fmt.acceptable;
this.formatters = fmt.formatters;
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 && PROXY_EVENTS.indexOf('upgrade') === -1) {
PROXY_EVENTS.push('upgrade');
}
PROXY_EVENTS.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._setupRequest(req, res);
self._handle(req, res, true);
});
if (options.handleUpgrades) {
this.server.on('upgrade', function onUpgrade(req, socket, head) {
req._upgradeRequest = true;
var res = upgrade.createResponse(req, socket, head);
self._setupRequest(req, res);
self._handle(req, res);
});
}
this.server.on('request', function onRequest(req, res) {
self.emit('request', req, res);
if (options.socketio && /^\/socket\.io.*/.test(req.url)) {
return;
}
self._setupRequest(req, res);
self._handle(req, res);
});
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|Regexp} path a string or regex matching the route
* @property {String|String[]} version versions supported by this route
* @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(/^\/([a-zA-Z0-9_\.~-]+)\/(.*)/, function(req, res, next) {});
* // an options object
* server.get({
* path: '/foo',
* version: ['1.0.0', '2.0.0']
* }, 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(h) {
self.before.push(h);
});
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(h) {
self.chain.push(h);
});
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;
};
/**
* Piggy-backs on the `server.use` method. It attaches a new middleware
* function that only fires if the specified version matches the request.
*
* Note that if the client does not request a specific version, the middleware
* function always fires. If you don't want this set a default version with a
* pre handler on requests where the client omits one.
*
* Exposes an API:
* server.versionedUse("version", function (req, res, next, ver) {
* // do stuff that only applies to routes of this API version
* });
*
* @public
* @memberof Server
* @instance
* @function versionedUse
* @param {String|Array} versions - the version(s) the URL to respond to
* @param {Function} fn - the middleware function to execute, the
* fourth parameter will be the selected
* version
* @returns {undefined} no return value
*/
Server.prototype.versionedUse = function versionedUse(versions, fn) {
if (!Array.isArray(versions)) {
versions = [versions];
}
assert.arrayOfString(versions, 'versions');
versions.forEach(function forEach(v) {
if (!semver.valid(v)) {
throw new TypeError('%s is not a valid semver', v);
}
});
this.use(function _versionedUse(req, res, next) {
var ver;
if (
req.version() === '*' ||
(ver = maxSatisfying(versions, req.version()) || false)
) {
fn.call(this, req, res, next, ver);
} 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} route - the route name.
* @returns {Boolean} true if route was removed, false if not.
*/
Server.prototype.rm = function rm(route) {
var r = this.router.unmount(route);
if (r && this.routes[r]) {
delete this.routes[r];
}
return r;
};
///--- 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,
* versions: 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();
// get each route's handler chain
_.forEach(routeInfo, function forEach(value, key) {
var routeName = value.name;
value.handlers = self.routes[routeName].map(funcNameMapper);
});
self._debugInfo = {
routes: routeInfo,
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: self.before.map(funcNameMapper),
use: self.chain.map(funcNameMapper),
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 self = this;
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.before));
str += sprintf(LINE_FMT, 'Router', this.router.toString());
str += sprintf(LINE_FMT, 'Routes', '');
Object.keys(this.routes).forEach(function forEach(k) {
var handlers = handlersToString(self.routes[k]);
str += sprintf(SUB_LINE_FMT, k, handlers);
});
str += sprintf(LINE_FMT, 'Secure', this.secure);
str += sprintf(LINE_FMT, 'Url', this.url);
str += sprintf(
LINE_FMT,
'Version',
Array.isArray(this.versions) ? this.versions.join() : this.versions
);
return str;
};
///--- Private methods
/**
* Route and run
*
* @private
* @memberof Server
* @instance
* @function _routeAndRun
* @param {Request} req - request
* @param {Response} res - response
* @returns {undefined} no return value
*/
Server.prototype._routeAndRun = function _routeAndRun(req, res) {
var self = this;
self._route(req, res, function _route(route, context) {
// emit 'routed' event after the req has been routed
self.emit('routed', req, res, route);
req.context = req.params = context;
req.route = route.spec;
var r = route ? route.name : null;
var chain = self.routes[r];
self._run(req, res, route, chain, function done(e) {
self._finishReqResCycle(req, res, route, e);
});
});
};
/**
* Upon receivng a request, route the request, then run the chain of handlers.
*
* @private
* @memberof Server
* @instance
* @function _handle
* @param {Object} req - the request object
* @param {Object} res - the response object
* @returns {undefined} no return value
*/
Server.prototype._handle = function _handle(req, res) {
var self = this;
// increment number of requests
self._inflightRequests++;
// emit 'pre' event before we run the pre handlers
self.emit('pre', req, res);
// run pre() handlers first before routing and running
if (self.before.length > 0) {
self._run(req, res, null, self.before, function _run(err) {
// Like with regular handlers, if we are provided an error, we
// should abort the middleware chain and fire after events.
if (err === false || err instanceof Error) {
self._finishReqResCycle(req, res, null, err);
return;
}
self._routeAndRun(req, res);
});
} else {
self._routeAndRun(req, res);
}
};
/**
* 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
* @returns {undefined} no return value
*/
Server.prototype._routeErrorResponse = function _routeErrorResponse(
req,
res,
err
) {
var self = this;
return self._emitErrorEvents(
req,
res,
null,
err,
function _emitErrorEvents() {
if (!res.headersSent) {
res.send(err);
}
return self._finishReqResCycle(req, res, null, err);
}
);
};
/**
* look into the router, find the route object that should match this request.
* if a route cannot be found, fire error events then flush the error out.
*
* @private
* @memberof Server
* @instance
* @function _route
* @param {Object} req - the request object
* @param {Object} res - the response object
* @param {String} [name] - name of the route
* @param {Function} cb - callback function
* @returns {undefined} no return value
*/
Server.prototype._route = function _route(req, res, name, cb) {
var self = this;
if (typeof name === 'function') {
cb = name;
name = null;
return this.router.find(req, res, function onRoute(err, route, ctx) {
var r = route ? route.name : null;
if (err) {
// TODO: if its a 404 for OPTION method (likely a CORS
// preflight), return OK. This should move into userland.
if (optionsError(err, req, res)) {
res.send(200);
return self._finishReqResCycle(req, res, null, null);
} else {
return self._routeErrorResponse(req, res, err);
}
} else if (!r || !self.routes[r]) {
err = new ResourceNotFoundError(req.path());
return self._routeErrorResponse(req, res, err);
} else {
// else no err, continue
return cb(route, ctx);
}
});
} else {
return this.router.get(name, req, function get(err, route, ctx) {
if (err) {
return self._routeErrorResponse(req, res, err);
} else {
// else no err, continue
return cb(route, ctx);
}
});
}
};
/**
* `cb()` is called when execution is complete. "completion" can occur when:
* 1) all functions in handler chain have been executed
* 2) users invoke `next(false)`. this indicates the chain should stop
* executing immediately.
* 3) users invoke `next(err)`. this is sugar for calling res.send(err) and
* firing any error events. after error events are done firing, it will also
* stop execution.
*
* The goofy checks in next() are to make sure we fire the DTrace
* probes after an error might have been sent, as in a handler
* return next(new Error) is basically shorthand for sending an
* error via res.send(), so we do that before firing the dtrace
* probe (namely so the status codes get updated in the
* response).
*
* there are two important closure variables in this function as a result of
* the way `next()` is currently implemented. `next()` assumes logic is sync,
* and automatically calls cb() when a route is considered complete. however,
* for case #3, emitted error events are async and serial. this means the
* automatic invocation of cb() cannot occur:
*
* 1) `emittingErrors` - this boolean is set to true when the server is still
* emitting error events. this var is used to avoid automatic invocation of
* cb(), which is delayed until all async error events are fired.
* 2) `done` - when next is invoked with a value of `false`, or handler if
*
* @private
* @memberof Server
* @instance
* @function _run
* @param {Object} req - the request object
* @param {Object} res - the response object
* @param {Object} route - the route object
* @param {Function[]} chain - array of handler functions
* @param {Function} cb - callback function
* @fires redirect
* @returns {undefined} no return value
*/
Server.prototype._run = function _run(req, res, route, chain, cb) {
var i = -1;
var id = dtrace.nextId();
req._dtraceId = id;
if (!req._anonFuncCount) {
// Counter used to keep track of anonymous functions. Used when a
// handler function is anonymous. This ensures we're using a
// monotonically increasing int for anonymous handlers through out the
// the lifetime of this request
req._anonFuncCount = 0;
}
var log = this.log;
var self = this;
var handlerName = null;
var emittingErrors = false;
cb = self._once(cb);
// attach a listener for 'close' and 'aborted' events, this will let us set
// a flag so that we can stop processing the request if the client closes
// the connection (or we lose the connection).
function _requestClose() {
req._connectionState = 'close';
}
function _requestAborted() {
req._connectionState = 'aborted';
}
req.once('close', _requestClose);
req.once('aborted', _requestAborted);
// attach a listener for the response's 'redirect' event
res.on('redirect', function onRedirect(redirectLocation) {
self.emit('redirect', redirectLocation);
});
function next(arg) {
// default value of done determined by whether or not there is another
// function in the chain and/or if req was not already closed. we will
// consume the value of `done` after dealing with any passed in values
// of `arg`.
var done = false;
if (typeof arg !== 'undefined') {
if (arg instanceof Error) {
// the emitting of the error events are async, so we can not
// complete this invocation of run() until it returns. set a
// flag so that the automatic invocation of cb() at the end of
// this function is bypassed.
emittingErrors = true;
// set the done flag - allows us to stop execution of handler
// chain now that an error has occurred.
done = true;
// now emit error events in serial and async
self._emitErrorEvents(
req,
res,
route,
arg,
function emitErrorsDone() {
res.send(arg);
return cb(arg);
}
);
} else if (typeof arg === 'string') {
// GH-193, allow redirect
if (req._rstfy_chained_route) {
var _e = new errors.InternalError();
log.error(
{
err: _e
},
'Multiple next("chain") calls not ' + 'supported'
);
res.send(_e);
return false;
}
// Stop running the rest of this route since we're redirecting.
// do this instead of setting done since the route technically
// isn't complete yet.
return self._route(req, res, arg, function _route(r, ctx) {
req.context = req.params = ctx;
req.route = r.spec;
var _c = chain.slice(0, i + 1);
function _uniq(fn) {
return _c.indexOf(fn) === -1;
}
var _routes = self.routes[r.name] || [];
var _chain = _routes.filter(_uniq);
req._rstfy_chained_route = true;
// Need to fire DTrace done for previous handler here too.
if (i + 1 > 0 && chain[i] && !chain[i]._skip) {
req.endHandlerTimer(handlerName);
}
self._run(req, res, r, _chain, cb);
});
} else if (arg === false) {
done = true;
}
}
// Fire DTrace done for the previous handler.
if (i + 1 > 0 && chain[i] && !chain[i]._skip) {
req.endHandlerTimer(handlerName);
}
// Run the next handler up
if (!done && chain[++i] && !_reqClosed(req)) {
if (chain[i]._skip) {
return next();
}
if (log.trace()) {
log.trace('running %s', chain[i].name || '?');
}
req._currentRoute = route !== null ? route.name : 'pre';
handlerName = chain[i].name || 'handler-' + req._anonFuncCount++;
req._currentHandler = handlerName;
req.startHandlerTimer(handlerName);
var n = self._once(next);
// support ifError only if domains are on
if (self.handleUncaughtExceptions === true) {
n.ifError = ifError(n);
}
return chain[i].call(self, req, res, n);
}
// if we have reached this last section of next(), then we are 'done'
// with this route.
dtrace._rstfy_probes['route-done'].fire(function fire() {
return [
self.name,
route !== null ? route.name : 'pre',
id,
res.statusCode || 200,
res.headers()
];
});
// if there is no route, it's because this is running the `pre` handler
// chain.
if (route === null) {
self.emit('preDone', req, res);
} else {
req.removeListener('close', _requestClose);
req.removeListener('aborted', _requestAborted);
self.emit('done', req, res, route);
}
// if we have reached here, there are no more handlers in the chain, or
// we next(err), and we are done with the request. if errors are still
// being emitted (due to being async), skip calling cb now, that will
// happen after all error events are done being emitted.
if (emittingErrors === false) {
return cb(arg);
}
// don't really need to return anything, returning null to placate
// eslint.
return null;
}
var n1 = self._once(next);
dtrace._rstfy_probes['route-start'].fire(function fire() {
return [
self.name,
route !== null ? route.name : 'pre',
id,
req.method,
req.href(),
req.headers
];
});
req.timers = [];
if (!self.handleUncaughtExceptions) {
return n1();
} else {
n1.ifError = ifError(n1);
// Add the uncaughtException error handler.
var d = domain.create();
d.add(req);
d.add(res);
d.on('error', function onError(err) {
if (err._restify_next) {
err._restify_next(err);
} else {
log.trace({ err: err }, 'uncaughtException');
self.emit('uncaughtException', req, res, route, err);
self._finishReqResCycle(req, res, route, err);
}
});
return d.run(n1);
}
};
/**
* 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;
req.log = res.log = self.log;
req._date = new Date();
req._time = process.hrtime();
req.serverName = self.name;
res.acceptable = self.acceptable;
res.formatters = self.formatters;
res.req = req;
res.serverName = self.name;
// set header only if name isn't empty string
if (self.name !== '') {
res.setHeader('Server', self.name);
}
res.version = self.router.versions[self.router.versions.length - 1];
};
/**
* 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;
var errName = errEvtNameFromError(err);
req.log.trace(
{
err: err,
errName: errName
},
'entering emitErrorEvents',
err.name
);
var errEvtNames = [];
// if we have listeners for the specific error, fire those first.
if (self.listeners(errName).length > 0) {
errEvtNames.push(errName);
}
// 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(nullErr, results) {
// vasync will never return error here. callback with the original
// error to pass it on.
return cb(err);
}
);
};
/**
* Wrapper method for emitting the after event. this is needed in scenarios
* where the async formatter has failed, and the ot assume that the
* original res.send() status code is no longer valid (it is now a 500). 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} [route] - the matched route
* @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,
route,
err
) {
var self = this;
// res.finished is set by node core's response object, when
// res.end() completes. if the request was closed by the client, then emit
// regardless of res status.
// after event has signature of function(req, res, route, err) {...}
if (!res.finished && !_reqClosed(req)) {
res.once('finish', function resFinished() {
self.emit('after', req, res, route, err || res.formatterErr);
});
} else {
// if there was an error in the processing of that request, use it.
// if not, check to see if the request was closed or aborted early and
// create an error out of that for audit logging.
var afterErr = err;
if (!afterErr) {
if (req._connectionState === 'close') {
afterErr = new errors.RequestCloseError();
} else if (req._connectionState === 'aborted') {
afterErr = new errors.RequestAbortedError();
}
}
self.emit('after', req, res, route, afterErr);
}
// decrement number of requests
self._inflightRequests--;
};
///--- Helpers
/**
* Helper function that returns true if the request was closed or aborted.
*
* @private
* @function _reqClosed
* @param {Object} req - the request object
* @returns {Boolean} is req closed or aborted
*/
function _reqClosed(req) {
return (
req._connectionState === 'close' || req._connectionState === 'aborted'
);
}
/**
* 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.lookup(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
};
}
/**
* Attaches ifError function attached to the `next` function in handler chain.
* uses a closure to maintain ref to next.
*
* @private
* @deprecated since 5.x
* @function ifError
* @param {Function} next - the next function
* @returns {Function} error handler
*/
function ifError(next) {
/**
* @throws will throw if an error is passed in.
* @private
* @function _ifError
* @param {Object} err - an error object
* @returns {undefined} no return value
*/
function _ifError(err) {
if (err) {
err._restify_next = next;
throw err;
}
}
return _ifError;
}
/**
* Returns true if the router generated a 404 for an options request.
*
* TODO: this is relevant for CORS only. Should move this out eventually to a
* userland middleware? This also seems a little like overreach, as there is no
* option to opt out of this behavior today.
*
* @private
* @function optionsError
* @param {Object} err - an error object
* @param {Object} req - the request object
* @param {Object} res - the response object
* @returns {Boolean} is options error
*/
function optionsError(err, req, res) {
return (
err.statusCode === 404 && req.method === 'OPTIONS' && req.url === '*'
);
}
/**
* 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 {
return err.name.replace(/Error$/, '');
}
}
/**
* 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');
}
var chain = [];
var route;
var self = this;
function addHandler(h) {
assert.func(h, 'handler');
chain.push(h);
}
opts.method = method;
opts.versions = opts.versions || opts.version || self.versions;
if (!Array.isArray(opts.versions)) {
opts.versions = [opts.versions];
}
if (!opts.name) {
opts.name = method + '-' + (opts.path || opts.url);
if (opts.versions.length > 0) {
opts.name += '-' + opts.versions.join('--');
}
opts.name = opts.name.replace(/\W/g, '').toLowerCase();
if (this.router.mounts[opts.name]) {
// GH-401
opts.name += uuid.v4().substr(0, 7);
}
}
if (!(route = this.router.mount(opts))) {
return false;
}
this.chain.forEach(addHandler);
// We accept both a variable number of handler functions, a
// variable number of nested arrays of handler functions, or a mix
// of both
argumentsToChain(Array.prototype.slice.call(arguments, 1)).forEach(
addHandler
);
this.routes[route] = chain;
return route;
};
}