layer.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. /*!
  2. * router
  3. * Copyright(c) 2013 Roman Shtylman
  4. * Copyright(c) 2014-2022 Douglas Christopher Wilson
  5. * MIT Licensed
  6. */
  7. 'use strict'
  8. /**
  9. * Module dependencies.
  10. * @private
  11. */
  12. const isPromise = require('is-promise')
  13. const pathRegexp = require('path-to-regexp')
  14. const debug = require('debug')('router:layer')
  15. const deprecate = require('depd')('router')
  16. /**
  17. * Module variables.
  18. * @private
  19. */
  20. const TRAILING_SLASH_REGEXP = /\/+$/
  21. const MATCHING_GROUP_REGEXP = /\((?:\?<(.*?)>)?(?!\?)/g
  22. /**
  23. * Expose `Layer`.
  24. */
  25. module.exports = Layer
  26. function Layer (path, options, fn) {
  27. if (!(this instanceof Layer)) {
  28. return new Layer(path, options, fn)
  29. }
  30. debug('new %o', path)
  31. const opts = options || {}
  32. this.handle = fn
  33. this.keys = []
  34. this.name = fn.name || '<anonymous>'
  35. this.params = undefined
  36. this.path = undefined
  37. this.slash = path === '/' && opts.end === false
  38. function matcher (_path) {
  39. if (_path instanceof RegExp) {
  40. const keys = []
  41. let name = 0
  42. let m
  43. // eslint-disable-next-line no-cond-assign
  44. while (m = MATCHING_GROUP_REGEXP.exec(_path.source)) {
  45. keys.push({
  46. name: m[1] || name++,
  47. offset: m.index
  48. })
  49. }
  50. return function regexpMatcher (p) {
  51. const match = _path.exec(p)
  52. if (!match) {
  53. return false
  54. }
  55. const params = {}
  56. for (let i = 1; i < match.length; i++) {
  57. const key = keys[i - 1]
  58. const prop = key.name
  59. const val = decodeParam(match[i])
  60. if (val !== undefined) {
  61. params[prop] = val
  62. }
  63. }
  64. return {
  65. params,
  66. path: match[0]
  67. }
  68. }
  69. }
  70. return pathRegexp.match((opts.strict ? _path : loosen(_path)), {
  71. sensitive: opts.sensitive,
  72. end: opts.end,
  73. trailing: !opts.strict,
  74. decode: decodeParam
  75. })
  76. }
  77. this.matchers = Array.isArray(path) ? path.map(matcher) : [matcher(path)]
  78. }
  79. /**
  80. * Handle the error for the layer.
  81. *
  82. * @param {Error} error
  83. * @param {Request} req
  84. * @param {Response} res
  85. * @param {function} next
  86. * @api private
  87. */
  88. Layer.prototype.handleError = function handleError (error, req, res, next) {
  89. const fn = this.handle
  90. if (fn.length !== 4) {
  91. // not a standard error handler
  92. return next(error)
  93. }
  94. try {
  95. // invoke function
  96. const ret = fn(error, req, res, next)
  97. // wait for returned promise
  98. if (isPromise(ret)) {
  99. if (!(ret instanceof Promise)) {
  100. deprecate('handlers that are Promise-like are deprecated, use a native Promise instead')
  101. }
  102. ret.then(null, function (error) {
  103. next(error || new Error('Rejected promise'))
  104. })
  105. }
  106. } catch (err) {
  107. next(err)
  108. }
  109. }
  110. /**
  111. * Handle the request for the layer.
  112. *
  113. * @param {Request} req
  114. * @param {Response} res
  115. * @param {function} next
  116. * @api private
  117. */
  118. Layer.prototype.handleRequest = function handleRequest (req, res, next) {
  119. const fn = this.handle
  120. if (fn.length > 3) {
  121. // not a standard request handler
  122. return next()
  123. }
  124. try {
  125. // invoke function
  126. const ret = fn(req, res, next)
  127. // wait for returned promise
  128. if (isPromise(ret)) {
  129. if (!(ret instanceof Promise)) {
  130. deprecate('handlers that are Promise-like are deprecated, use a native Promise instead')
  131. }
  132. ret.then(null, function (error) {
  133. next(error || new Error('Rejected promise'))
  134. })
  135. }
  136. } catch (err) {
  137. next(err)
  138. }
  139. }
  140. /**
  141. * Check if this route matches `path`, if so
  142. * populate `.params`.
  143. *
  144. * @param {String} path
  145. * @return {Boolean}
  146. * @api private
  147. */
  148. Layer.prototype.match = function match (path) {
  149. let match
  150. if (path != null) {
  151. // fast path non-ending match for / (any path matches)
  152. if (this.slash) {
  153. this.params = {}
  154. this.path = ''
  155. return true
  156. }
  157. let i = 0
  158. while (!match && i < this.matchers.length) {
  159. // match the path
  160. match = this.matchers[i](path)
  161. i++
  162. }
  163. }
  164. if (!match) {
  165. this.params = undefined
  166. this.path = undefined
  167. return false
  168. }
  169. // store values
  170. this.params = match.params
  171. this.path = match.path
  172. this.keys = Object.keys(match.params)
  173. return true
  174. }
  175. /**
  176. * Decode param value.
  177. *
  178. * @param {string} val
  179. * @return {string}
  180. * @private
  181. */
  182. function decodeParam (val) {
  183. if (typeof val !== 'string' || val.length === 0) {
  184. return val
  185. }
  186. try {
  187. return decodeURIComponent(val)
  188. } catch (err) {
  189. if (err instanceof URIError) {
  190. err.message = 'Failed to decode param \'' + val + '\''
  191. err.status = 400
  192. }
  193. throw err
  194. }
  195. }
  196. /**
  197. * Loosens the given path for path-to-regexp matching.
  198. */
  199. function loosen (path) {
  200. if (path instanceof RegExp || path === '/') {
  201. return path
  202. }
  203. return Array.isArray(path)
  204. ? path.map(function (p) { return loosen(p) })
  205. : String(path).replace(TRAILING_SLASH_REGEXP, '')
  206. }