// 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 * You can call like: * 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 * For example, `pre()` can be used to deduplicate slashes in * URLs * 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 * Output: * { 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 * Output: * { * 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 * Output: * 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; }; }