/** * Module dependencies. */ var Route = require('./route'); var Layer = require('./layer'); var methods = require('methods'); var debug = require('debug')('express:router'); var parseUrl = require('parseurl'); var slice = Array.prototype.slice; /** * Initialize a new `Router` with the given `options`. * * @param {Object} options * @return {Router} which is an callable function * @api public */ var proto = module.exports = function(options) { options = options || {}; function router(req, res, next) { router.handle(req, res, next); } // mixin Router class functions router.__proto__ = proto; router.params = {}; router._params = []; router.caseSensitive = options.caseSensitive; router.strict = options.strict; router.stack = []; return router; }; /** * Map the given param placeholder `name`(s) to the given callback. * * Parameter mapping is used to provide pre-conditions to routes * which use normalized placeholders. For example a _:user_id_ parameter * could automatically load a user's information from the database without * any additional code, * * The callback uses the same signature as middleware, the only difference * being that the value of the placeholder is passed, in this case the _id_ * of the user. Once the `next()` function is invoked, just like middleware * it will continue on to execute the route, or subsequent parameter functions. * * Just like in middleware, you must either respond to the request or call next * to avoid stalling the request. * * app.param('user_id', function(req, res, next, id){ * User.find(id, function(err, user){ * if (err) { * return next(err); * } else if (!user) { * return next(new Error('failed to load user')); * } * req.user = user; * next(); * }); * }); * * @param {String} name * @param {Function} fn * @return {app} for chaining * @api public */ proto.param = function(name, fn){ // param logic if ('function' == typeof name) { this._params.push(name); return; } // apply param functions var params = this._params; var len = params.length; var ret; if (name[0] === ':') { name = name.substr(1); } for (var i = 0; i < len; ++i) { if (ret = params[i](name, fn)) { fn = ret; } } // ensure we end up with a // middleware function if ('function' != typeof fn) { throw new Error('invalid param() call for ' + name + ', got ' + fn); } (this.params[name] = this.params[name] || []).push(fn); return this; }; /** * Dispatch a req, res into the router. * * @api private */ proto.handle = function(req, res, done) { var self = this; debug('dispatching %s %s', req.method, req.url); var method = req.method.toLowerCase(); var search = 1 + req.url.indexOf('?'); var pathlength = search ? search - 1 : req.url.length; var fqdn = 1 + req.url.substr(0, pathlength).indexOf('://'); var protohost = fqdn ? req.url.substr(0, req.url.indexOf('/', 2 + fqdn)) : ''; var idx = 0; var removed = ''; var slashAdded = false; var paramcalled = {}; // store options for OPTIONS request // only used if OPTIONS request var options = []; // middleware and routes var stack = self.stack; // manage inter-router variables var parent = req.next; var parentUrl = req.baseUrl || ''; done = wrap(done, function(old, err) { req.baseUrl = parentUrl; req.next = parent; old(err); }); req.next = next; // for options requests, respond with a default if nothing else responds if (method === 'options') { done = wrap(done, function(old, err) { if (err || options.length === 0) return old(err); var body = options.join(','); return res.set('Allow', body).send(body); }); } next(); function next(err) { if (err === 'route') { err = undefined; } var layer = stack[idx++]; var layerPath; if (!layer) { return done(err); } if (slashAdded) { req.url = req.url.substr(1); slashAdded = false; } req.baseUrl = parentUrl; req.url = protohost + removed + req.url.substr(protohost.length); req.originalUrl = req.originalUrl || req.url; removed = ''; try { var path = parseUrl(req).pathname; if (undefined == path) path = '/'; if (!layer.match(path)) return next(err); // route object and not middleware var route = layer.route; // if final route, then we support options if (route) { // we don't run any routes with error first if (err) { return next(err); } req.route = route; // we can now dispatch to the route if (method === 'options' && !route.methods['options']) { options.push.apply(options, route._options()); } } // Capture one-time layer values req.params = layer.params; layerPath = layer.path; // this should be done for the layer return self.process_params(layer, paramcalled, req, res, function(err) { if (err) { return next(err); } if (route) { return layer.handle(req, res, next); } trim_prefix(); }); } catch (err) { next(err); } function trim_prefix() { var c = path[layerPath.length]; if (c && '/' != c && '.' != c) return next(err); // Trim off the part of the url that matches the route // middleware (.use stuff) needs to have the path stripped removed = layerPath; if (removed.length) { debug('trim prefix (%s) from url %s', layerPath, req.url); req.url = protohost + req.url.substr(protohost.length + removed.length); } // Ensure leading slash if (!fqdn && req.url[0] !== '/') { req.url = '/' + req.url; slashAdded = true; } // Setup base URL (no trailing slash) if (removed.length && removed.substr(-1) === '/') { req.baseUrl = parentUrl + removed.substring(0, removed.length - 1); } else { req.baseUrl = parentUrl + removed; } debug('%s %s : %s', layer.handle.name || 'anonymous', layerPath, req.originalUrl); var arity = layer.handle.length; try { if (err && arity === 4) { layer.handle(err, req, res, next); } else if (!err && arity < 4) { layer.handle(req, res, next); } else { next(err); } } catch (err) { next(err); } } } function wrap(old, fn) { return function () { var args = [old].concat(slice.call(arguments)); fn.apply(this, args); }; } }; /** * Process any parameters for the layer. * * @api private */ proto.process_params = function(layer, called, req, res, done) { var params = this.params; // captured parameters from the layer, keys and values var keys = layer.keys; // fast track if (!keys || keys.length === 0) { return done(); } var i = 0; var name; var paramIndex = 0; var key; var paramVal; var paramCallbacks; var paramCalled; // process params in order // param callbacks can be async function param(err) { if (err) { return done(err); } if (i >= keys.length ) { return done(); } paramIndex = 0; key = keys[i++]; if (!key) { return done(); } name = key.name; paramVal = req.params[name]; paramCallbacks = params[name]; paramCalled = called[name]; if (paramVal === undefined || !paramCallbacks) { return param(); } // param previously called with same value or error occurred if (paramCalled && (paramCalled.error || paramCalled.match === paramVal)) { // restore value req.params[name] = paramCalled.value; // next param return param(paramCalled.error); } called[name] = paramCalled = { error: null, match: paramVal, value: paramVal }; try { return paramCallback(); } catch (err) { return done(err); } } // single param callbacks function paramCallback(err) { var fn = paramCallbacks[paramIndex++]; // store updated value paramCalled.value = req.params[key.name]; if (err) { // store error paramCalled.error = err; param(err); return; } if (!fn) return param(); fn(req, res, paramCallback, paramVal, key.name); } param(); }; /** * Use the given middleware function, with optional path, defaulting to "/". * * Use (like `.all`) will run for any http METHOD, but it will not add * handlers for those methods so OPTIONS requests will not consider `.use` * functions even if they could respond. * * The other difference is that _route_ path is stripped and not visible * to the handler function. The main effect of this feature is that mounted * handlers can operate without any code changes regardless of the "prefix" * pathname. * * @param {String|Function} route * @param {Function} fn * @return {app} for chaining * @api public */ proto.use = function(route, fn){ // default route to '/' if ('string' != typeof route) { fn = route; route = '/'; } if (typeof fn !== 'function') { var type = {}.toString.call(fn); var msg = 'Router.use() requires callback functions but got a ' + type; throw new Error(msg); } // strip trailing slash if ('/' == route[route.length - 1]) { route = route.slice(0, -1); } var layer = new Layer(route, { sensitive: this.caseSensitive, strict: this.strict, end: false }, fn); // add the middleware debug('use %s %s', route || '/', fn.name || 'anonymous'); this.stack.push(layer); return this; }; /** * Create a new Route for the given path. * * Each route contains a separate middleware stack and VERB handlers. * * See the Route api documentation for details on adding handlers * and middleware to routes. * * @param {String} path * @return {Route} * @api public */ proto.route = function(path){ var route = new Route(path); var layer = new Layer(path, { sensitive: this.caseSensitive, strict: this.strict, end: true }, route.dispatch.bind(route)); layer.route = route; this.stack.push(layer); return route; }; // create Router#VERB functions methods.concat('all').forEach(function(method){ proto[method] = function(path){ var route = this.route(path) route[method].apply(route, [].slice.call(arguments, 1)); return this; }; });