index.js
1 /*! 2 * router 3 * Copyright(c) 2013 Roman Shtylman 4 * Copyright(c) 2014-2022 Douglas Christopher Wilson 5 * MIT Licensed 6 */ 7 8 'use strict' 9 10 /** 11 * Module dependencies. 12 * @private 13 */ 14 15 const isPromise = require('is-promise') 16 const Layer = require('./lib/layer') 17 const { METHODS } = require('node:http') 18 const parseUrl = require('parseurl') 19 const Route = require('./lib/route') 20 const debug = require('debug')('router') 21 const deprecate = require('depd')('router') 22 23 /** 24 * Module variables. 25 * @private 26 */ 27 28 const slice = Array.prototype.slice 29 const flatten = Array.prototype.flat 30 const methods = METHODS.map((method) => method.toLowerCase()) 31 32 /** 33 * Expose `Router`. 34 */ 35 36 module.exports = Router 37 38 /** 39 * Expose `Route`. 40 */ 41 42 module.exports.Route = Route 43 44 /** 45 * Initialize a new `Router` with the given `options`. 46 * 47 * @param {object} [options] 48 * @return {Router} which is a callable function 49 * @public 50 */ 51 52 function Router (options) { 53 if (!(this instanceof Router)) { 54 return new Router(options) 55 } 56 57 const opts = options || {} 58 59 function router (req, res, next) { 60 router.handle(req, res, next) 61 } 62 63 // inherit from the correct prototype 64 Object.setPrototypeOf(router, this) 65 66 router.caseSensitive = opts.caseSensitive 67 router.mergeParams = opts.mergeParams 68 router.params = {} 69 router.strict = opts.strict 70 router.stack = [] 71 72 return router 73 } 74 75 /** 76 * Router prototype inherits from a Function. 77 */ 78 79 /* istanbul ignore next */ 80 Router.prototype = function () {} 81 82 /** 83 * Map the given param placeholder `name`(s) to the given callback. 84 * 85 * Parameter mapping is used to provide pre-conditions to routes 86 * which use normalized placeholders. For example a _:user_id_ parameter 87 * could automatically load a user's information from the database without 88 * any additional code. 89 * 90 * The callback uses the same signature as middleware, the only difference 91 * being that the value of the placeholder is passed, in this case the _id_ 92 * of the user. Once the `next()` function is invoked, just like middleware 93 * it will continue on to execute the route, or subsequent parameter functions. 94 * 95 * Just like in middleware, you must either respond to the request or call next 96 * to avoid stalling the request. 97 * 98 * router.param('user_id', function(req, res, next, id){ 99 * User.find(id, function(err, user){ 100 * if (err) { 101 * return next(err) 102 * } else if (!user) { 103 * return next(new Error('failed to load user')) 104 * } 105 * req.user = user 106 * next() 107 * }) 108 * }) 109 * 110 * @param {string} name 111 * @param {function} fn 112 * @public 113 */ 114 115 Router.prototype.param = function param (name, fn) { 116 if (!name) { 117 throw new TypeError('argument name is required') 118 } 119 120 if (typeof name !== 'string') { 121 throw new TypeError('argument name must be a string') 122 } 123 124 if (!fn) { 125 throw new TypeError('argument fn is required') 126 } 127 128 if (typeof fn !== 'function') { 129 throw new TypeError('argument fn must be a function') 130 } 131 132 let params = this.params[name] 133 134 if (!params) { 135 params = this.params[name] = [] 136 } 137 138 params.push(fn) 139 140 return this 141 } 142 143 /** 144 * Dispatch a req, res into the router. 145 * 146 * @private 147 */ 148 149 Router.prototype.handle = function handle (req, res, callback) { 150 if (!callback) { 151 throw new TypeError('argument callback is required') 152 } 153 154 debug('dispatching %s %s', req.method, req.url) 155 156 let idx = 0 157 let methods 158 const protohost = getProtohost(req.url) || '' 159 let removed = '' 160 const self = this 161 let slashAdded = false 162 let sync = 0 163 const paramcalled = {} 164 165 // middleware and routes 166 const stack = this.stack 167 168 // manage inter-router variables 169 const parentParams = req.params 170 const parentUrl = req.baseUrl || '' 171 let done = restore(callback, req, 'baseUrl', 'next', 'params') 172 173 // setup next layer 174 req.next = next 175 176 // for options requests, respond with a default if nothing else responds 177 if (req.method === 'OPTIONS') { 178 methods = [] 179 done = wrap(done, generateOptionsResponder(res, methods)) 180 } 181 182 // setup basic req values 183 req.baseUrl = parentUrl 184 req.originalUrl = req.originalUrl || req.url 185 186 next() 187 188 function next (err) { 189 let layerError = err === 'route' 190 ? null 191 : err 192 193 // remove added slash 194 if (slashAdded) { 195 req.url = req.url.slice(1) 196 slashAdded = false 197 } 198 199 // restore altered req.url 200 if (removed.length !== 0) { 201 req.baseUrl = parentUrl 202 req.url = protohost + removed + req.url.slice(protohost.length) 203 removed = '' 204 } 205 206 // signal to exit router 207 if (layerError === 'router') { 208 setImmediate(done, null) 209 return 210 } 211 212 // no more matching layers 213 if (idx >= stack.length) { 214 setImmediate(done, layerError) 215 return 216 } 217 218 // max sync stack 219 if (++sync > 100) { 220 return setImmediate(next, err) 221 } 222 223 // get pathname of request 224 const path = getPathname(req) 225 226 if (path == null) { 227 return done(layerError) 228 } 229 230 // find next matching layer 231 let layer 232 let match 233 let route 234 235 while (match !== true && idx < stack.length) { 236 layer = stack[idx++] 237 match = matchLayer(layer, path) 238 route = layer.route 239 240 if (typeof match !== 'boolean') { 241 // hold on to layerError 242 layerError = layerError || match 243 } 244 245 if (match !== true) { 246 continue 247 } 248 249 if (!route) { 250 // process non-route handlers normally 251 continue 252 } 253 254 if (layerError) { 255 // routes do not match with a pending error 256 match = false 257 continue 258 } 259 260 const method = req.method 261 const hasMethod = route._handlesMethod(method) 262 263 // build up automatic options response 264 if (!hasMethod && method === 'OPTIONS' && methods) { 265 methods.push.apply(methods, route._methods()) 266 } 267 268 // don't even bother matching route 269 if (!hasMethod && method !== 'HEAD') { 270 match = false 271 } 272 } 273 274 // no match 275 if (match !== true) { 276 return done(layerError) 277 } 278 279 // store route for dispatch on change 280 if (route) { 281 req.route = route 282 } 283 284 // Capture one-time layer values 285 req.params = self.mergeParams 286 ? mergeParams(layer.params, parentParams) 287 : layer.params 288 const layerPath = layer.path 289 290 // this should be done for the layer 291 processParams(self.params, layer, paramcalled, req, res, function (err) { 292 if (err) { 293 next(layerError || err) 294 } else if (route) { 295 layer.handleRequest(req, res, next) 296 } else { 297 trimPrefix(layer, layerError, layerPath, path) 298 } 299 300 sync = 0 301 }) 302 } 303 304 function trimPrefix (layer, layerError, layerPath, path) { 305 if (layerPath.length !== 0) { 306 // Validate path is a prefix match 307 if (layerPath !== path.substring(0, layerPath.length)) { 308 next(layerError) 309 return 310 } 311 312 // Validate path breaks on a path separator 313 const c = path[layerPath.length] 314 if (c && c !== '/') { 315 next(layerError) 316 return 317 } 318 319 // Trim off the part of the url that matches the route 320 // middleware (.use stuff) needs to have the path stripped 321 debug('trim prefix (%s) from url %s', layerPath, req.url) 322 removed = layerPath 323 req.url = protohost + req.url.slice(protohost.length + removed.length) 324 325 // Ensure leading slash 326 if (!protohost && req.url[0] !== '/') { 327 req.url = '/' + req.url 328 slashAdded = true 329 } 330 331 // Setup base URL (no trailing slash) 332 req.baseUrl = parentUrl + (removed[removed.length - 1] === '/' 333 ? removed.substring(0, removed.length - 1) 334 : removed) 335 } 336 337 debug('%s %s : %s', layer.name, layerPath, req.originalUrl) 338 339 if (layerError) { 340 layer.handleError(layerError, req, res, next) 341 } else { 342 layer.handleRequest(req, res, next) 343 } 344 } 345 } 346 347 /** 348 * Use the given middleware function, with optional path, defaulting to "/". 349 * 350 * Use (like `.all`) will run for any http METHOD, but it will not add 351 * handlers for those methods so OPTIONS requests will not consider `.use` 352 * functions even if they could respond. 353 * 354 * The other difference is that _route_ path is stripped and not visible 355 * to the handler function. The main effect of this feature is that mounted 356 * handlers can operate without any code changes regardless of the "prefix" 357 * pathname. 358 * 359 * @public 360 */ 361 362 Router.prototype.use = function use (handler) { 363 let offset = 0 364 let path = '/' 365 366 // default path to '/' 367 // disambiguate router.use([handler]) 368 if (typeof handler !== 'function') { 369 let arg = handler 370 371 while (Array.isArray(arg) && arg.length !== 0) { 372 arg = arg[0] 373 } 374 375 // first arg is the path 376 if (typeof arg !== 'function') { 377 offset = 1 378 path = handler 379 } 380 } 381 382 const callbacks = flatten.call(slice.call(arguments, offset), Infinity) 383 384 if (callbacks.length === 0) { 385 throw new TypeError('argument handler is required') 386 } 387 388 for (let i = 0; i < callbacks.length; i++) { 389 const fn = callbacks[i] 390 391 if (typeof fn !== 'function') { 392 throw new TypeError('argument handler must be a function') 393 } 394 395 // add the middleware 396 debug('use %o %s', path, fn.name || '<anonymous>') 397 398 const layer = new Layer(path, { 399 sensitive: this.caseSensitive, 400 strict: false, 401 end: false 402 }, fn) 403 404 layer.route = undefined 405 406 this.stack.push(layer) 407 } 408 409 return this 410 } 411 412 /** 413 * Create a new Route for the given path. 414 * 415 * Each route contains a separate middleware stack and VERB handlers. 416 * 417 * See the Route api documentation for details on adding handlers 418 * and middleware to routes. 419 * 420 * @param {string} path 421 * @return {Route} 422 * @public 423 */ 424 425 Router.prototype.route = function route (path) { 426 const route = new Route(path) 427 428 const layer = new Layer(path, { 429 sensitive: this.caseSensitive, 430 strict: this.strict, 431 end: true 432 }, handle) 433 434 function handle (req, res, next) { 435 route.dispatch(req, res, next) 436 } 437 438 layer.route = route 439 440 this.stack.push(layer) 441 return route 442 } 443 444 // create Router#VERB functions 445 methods.concat('all').forEach(function (method) { 446 Router.prototype[method] = function (path) { 447 const route = this.route(path) 448 route[method].apply(route, slice.call(arguments, 1)) 449 return this 450 } 451 }) 452 453 /** 454 * Generate a callback that will make an OPTIONS response. 455 * 456 * @param {OutgoingMessage} res 457 * @param {array} methods 458 * @private 459 */ 460 461 function generateOptionsResponder (res, methods) { 462 return function onDone (fn, err) { 463 if (err || methods.length === 0) { 464 return fn(err) 465 } 466 467 trySendOptionsResponse(res, methods, fn) 468 } 469 } 470 471 /** 472 * Get pathname of request. 473 * 474 * @param {IncomingMessage} req 475 * @private 476 */ 477 478 function getPathname (req) { 479 try { 480 return parseUrl(req).pathname 481 } catch (err) { 482 return undefined 483 } 484 } 485 486 /** 487 * Get get protocol + host for a URL. 488 * 489 * @param {string} url 490 * @private 491 */ 492 493 function getProtohost (url) { 494 if (typeof url !== 'string' || url.length === 0 || url[0] === '/') { 495 return undefined 496 } 497 498 const searchIndex = url.indexOf('?') 499 const pathLength = searchIndex !== -1 500 ? searchIndex 501 : url.length 502 const fqdnIndex = url.substring(0, pathLength).indexOf('://') 503 504 return fqdnIndex !== -1 505 ? url.substring(0, url.indexOf('/', 3 + fqdnIndex)) 506 : undefined 507 } 508 509 /** 510 * Match path to a layer. 511 * 512 * @param {Layer} layer 513 * @param {string} path 514 * @private 515 */ 516 517 function matchLayer (layer, path) { 518 try { 519 return layer.match(path) 520 } catch (err) { 521 return err 522 } 523 } 524 525 /** 526 * Merge params with parent params 527 * 528 * @private 529 */ 530 531 function mergeParams (params, parent) { 532 if (typeof parent !== 'object' || !parent) { 533 return params 534 } 535 536 // make copy of parent for base 537 const obj = Object.assign({}, parent) 538 539 // simple non-numeric merging 540 if (!(0 in params) || !(0 in parent)) { 541 return Object.assign(obj, params) 542 } 543 544 let i = 0 545 let o = 0 546 547 // determine numeric gap in params 548 while (i in params) { 549 i++ 550 } 551 552 // determine numeric gap in parent 553 while (o in parent) { 554 o++ 555 } 556 557 // offset numeric indices in params before merge 558 for (i--; i >= 0; i--) { 559 params[i + o] = params[i] 560 561 // create holes for the merge when necessary 562 if (i < o) { 563 delete params[i] 564 } 565 } 566 567 return Object.assign(obj, params) 568 } 569 570 /** 571 * Process any parameters for the layer. 572 * 573 * @private 574 */ 575 576 function processParams (params, layer, called, req, res, done) { 577 // captured parameters from the layer, keys and values 578 const keys = layer.keys 579 580 // fast track 581 if (!keys || keys.length === 0) { 582 return done() 583 } 584 585 let i = 0 586 let paramIndex = 0 587 let key 588 let paramVal 589 let paramCallbacks 590 let paramCalled 591 592 // process params in order 593 // param callbacks can be async 594 function param (err) { 595 if (err) { 596 return done(err) 597 } 598 599 if (i >= keys.length) { 600 return done() 601 } 602 603 paramIndex = 0 604 key = keys[i++] 605 paramVal = req.params[key] 606 paramCallbacks = params[key] 607 paramCalled = called[key] 608 609 if (paramVal === undefined || !paramCallbacks) { 610 return param() 611 } 612 613 // param previously called with same value or error occurred 614 if (paramCalled && (paramCalled.match === paramVal || 615 (paramCalled.error && paramCalled.error !== 'route'))) { 616 // restore value 617 req.params[key] = paramCalled.value 618 619 // next param 620 return param(paramCalled.error) 621 } 622 623 called[key] = paramCalled = { 624 error: null, 625 match: paramVal, 626 value: paramVal 627 } 628 629 paramCallback() 630 } 631 632 // single param callbacks 633 function paramCallback (err) { 634 const fn = paramCallbacks[paramIndex++] 635 636 // store updated value 637 paramCalled.value = req.params[key] 638 639 if (err) { 640 // store error 641 paramCalled.error = err 642 param(err) 643 return 644 } 645 646 if (!fn) return param() 647 648 try { 649 const ret = fn(req, res, paramCallback, paramVal, key) 650 if (isPromise(ret)) { 651 if (!(ret instanceof Promise)) { 652 deprecate('parameters that are Promise-like are deprecated, use a native Promise instead') 653 } 654 655 ret.then(null, function (error) { 656 paramCallback(error || new Error('Rejected promise')) 657 }) 658 } 659 } catch (e) { 660 paramCallback(e) 661 } 662 } 663 664 param() 665 } 666 667 /** 668 * Restore obj props after function 669 * 670 * @private 671 */ 672 673 function restore (fn, obj) { 674 const props = new Array(arguments.length - 2) 675 const vals = new Array(arguments.length - 2) 676 677 for (let i = 0; i < props.length; i++) { 678 props[i] = arguments[i + 2] 679 vals[i] = obj[props[i]] 680 } 681 682 return function () { 683 // restore vals 684 for (let i = 0; i < props.length; i++) { 685 obj[props[i]] = vals[i] 686 } 687 688 return fn.apply(this, arguments) 689 } 690 } 691 692 /** 693 * Send an OPTIONS response. 694 * 695 * @private 696 */ 697 698 function sendOptionsResponse (res, methods) { 699 const options = Object.create(null) 700 701 // build unique method map 702 for (let i = 0; i < methods.length; i++) { 703 options[methods[i]] = true 704 } 705 706 // construct the allow list 707 const allow = Object.keys(options).sort().join(', ') 708 709 // send response 710 res.setHeader('Allow', allow) 711 res.setHeader('Content-Length', Buffer.byteLength(allow)) 712 res.setHeader('Content-Type', 'text/plain') 713 res.setHeader('X-Content-Type-Options', 'nosniff') 714 res.end(allow) 715 } 716 717 /** 718 * Try to send an OPTIONS response. 719 * 720 * @private 721 */ 722 723 function trySendOptionsResponse (res, methods, next) { 724 try { 725 sendOptionsResponse(res, methods) 726 } catch (err) { 727 next(err) 728 } 729 } 730 731 /** 732 * Wrap a function 733 * 734 * @private 735 */ 736 737 function wrap (old, fn) { 738 return function proxy () { 739 const args = new Array(arguments.length + 1) 740 741 args[0] = old 742 for (let i = 0, len = arguments.length; i < len; i++) { 743 args[i + 1] = arguments[i] 744 } 745 746 fn.apply(this, args) 747 } 748 }