request.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. 'use strict'
  2. const {
  3. InvalidArgumentError,
  4. NotSupportedError
  5. } = require('./errors')
  6. const assert = require('node:assert')
  7. const {
  8. isValidHTTPToken,
  9. isValidHeaderValue,
  10. isStream,
  11. destroy,
  12. isBuffer,
  13. isFormDataLike,
  14. isIterable,
  15. isBlobLike,
  16. buildURL,
  17. validateHandler,
  18. getServerName,
  19. normalizedMethodRecords
  20. } = require('./util')
  21. const { channels } = require('./diagnostics.js')
  22. const { headerNameLowerCasedRecord } = require('./constants')
  23. // Verifies that a given path is valid does not contain control chars \x00 to \x20
  24. const invalidPathRegex = /[^\u0021-\u00ff]/
  25. const kHandler = Symbol('handler')
  26. class Request {
  27. constructor (origin, {
  28. path,
  29. method,
  30. body,
  31. headers,
  32. query,
  33. idempotent,
  34. blocking,
  35. upgrade,
  36. headersTimeout,
  37. bodyTimeout,
  38. reset,
  39. throwOnError,
  40. expectContinue,
  41. servername
  42. }, handler) {
  43. if (typeof path !== 'string') {
  44. throw new InvalidArgumentError('path must be a string')
  45. } else if (
  46. path[0] !== '/' &&
  47. !(path.startsWith('http://') || path.startsWith('https://')) &&
  48. method !== 'CONNECT'
  49. ) {
  50. throw new InvalidArgumentError('path must be an absolute URL or start with a slash')
  51. } else if (invalidPathRegex.test(path)) {
  52. throw new InvalidArgumentError('invalid request path')
  53. }
  54. if (typeof method !== 'string') {
  55. throw new InvalidArgumentError('method must be a string')
  56. } else if (normalizedMethodRecords[method] === undefined && !isValidHTTPToken(method)) {
  57. throw new InvalidArgumentError('invalid request method')
  58. }
  59. if (upgrade && typeof upgrade !== 'string') {
  60. throw new InvalidArgumentError('upgrade must be a string')
  61. }
  62. if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) {
  63. throw new InvalidArgumentError('invalid headersTimeout')
  64. }
  65. if (bodyTimeout != null && (!Number.isFinite(bodyTimeout) || bodyTimeout < 0)) {
  66. throw new InvalidArgumentError('invalid bodyTimeout')
  67. }
  68. if (reset != null && typeof reset !== 'boolean') {
  69. throw new InvalidArgumentError('invalid reset')
  70. }
  71. if (expectContinue != null && typeof expectContinue !== 'boolean') {
  72. throw new InvalidArgumentError('invalid expectContinue')
  73. }
  74. this.headersTimeout = headersTimeout
  75. this.bodyTimeout = bodyTimeout
  76. this.throwOnError = throwOnError === true
  77. this.method = method
  78. this.abort = null
  79. if (body == null) {
  80. this.body = null
  81. } else if (isStream(body)) {
  82. this.body = body
  83. const rState = this.body._readableState
  84. if (!rState || !rState.autoDestroy) {
  85. this.endHandler = function autoDestroy () {
  86. destroy(this)
  87. }
  88. this.body.on('end', this.endHandler)
  89. }
  90. this.errorHandler = err => {
  91. if (this.abort) {
  92. this.abort(err)
  93. } else {
  94. this.error = err
  95. }
  96. }
  97. this.body.on('error', this.errorHandler)
  98. } else if (isBuffer(body)) {
  99. this.body = body.byteLength ? body : null
  100. } else if (ArrayBuffer.isView(body)) {
  101. this.body = body.buffer.byteLength ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null
  102. } else if (body instanceof ArrayBuffer) {
  103. this.body = body.byteLength ? Buffer.from(body) : null
  104. } else if (typeof body === 'string') {
  105. this.body = body.length ? Buffer.from(body) : null
  106. } else if (isFormDataLike(body) || isIterable(body) || isBlobLike(body)) {
  107. this.body = body
  108. } else {
  109. throw new InvalidArgumentError('body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable')
  110. }
  111. this.completed = false
  112. this.aborted = false
  113. this.upgrade = upgrade || null
  114. this.path = query ? buildURL(path, query) : path
  115. this.origin = origin
  116. this.idempotent = idempotent == null
  117. ? method === 'HEAD' || method === 'GET'
  118. : idempotent
  119. this.blocking = blocking == null ? false : blocking
  120. this.reset = reset == null ? null : reset
  121. this.host = null
  122. this.contentLength = null
  123. this.contentType = null
  124. this.headers = []
  125. // Only for H2
  126. this.expectContinue = expectContinue != null ? expectContinue : false
  127. if (Array.isArray(headers)) {
  128. if (headers.length % 2 !== 0) {
  129. throw new InvalidArgumentError('headers array must be even')
  130. }
  131. for (let i = 0; i < headers.length; i += 2) {
  132. processHeader(this, headers[i], headers[i + 1])
  133. }
  134. } else if (headers && typeof headers === 'object') {
  135. if (headers[Symbol.iterator]) {
  136. for (const header of headers) {
  137. if (!Array.isArray(header) || header.length !== 2) {
  138. throw new InvalidArgumentError('headers must be in key-value pair format')
  139. }
  140. processHeader(this, header[0], header[1])
  141. }
  142. } else {
  143. const keys = Object.keys(headers)
  144. for (let i = 0; i < keys.length; ++i) {
  145. processHeader(this, keys[i], headers[keys[i]])
  146. }
  147. }
  148. } else if (headers != null) {
  149. throw new InvalidArgumentError('headers must be an object or an array')
  150. }
  151. validateHandler(handler, method, upgrade)
  152. this.servername = servername || getServerName(this.host)
  153. this[kHandler] = handler
  154. if (channels.create.hasSubscribers) {
  155. channels.create.publish({ request: this })
  156. }
  157. }
  158. onBodySent (chunk) {
  159. if (this[kHandler].onBodySent) {
  160. try {
  161. return this[kHandler].onBodySent(chunk)
  162. } catch (err) {
  163. this.abort(err)
  164. }
  165. }
  166. }
  167. onRequestSent () {
  168. if (channels.bodySent.hasSubscribers) {
  169. channels.bodySent.publish({ request: this })
  170. }
  171. if (this[kHandler].onRequestSent) {
  172. try {
  173. return this[kHandler].onRequestSent()
  174. } catch (err) {
  175. this.abort(err)
  176. }
  177. }
  178. }
  179. onConnect (abort) {
  180. assert(!this.aborted)
  181. assert(!this.completed)
  182. if (this.error) {
  183. abort(this.error)
  184. } else {
  185. this.abort = abort
  186. return this[kHandler].onConnect(abort)
  187. }
  188. }
  189. onResponseStarted () {
  190. return this[kHandler].onResponseStarted?.()
  191. }
  192. onHeaders (statusCode, headers, resume, statusText) {
  193. assert(!this.aborted)
  194. assert(!this.completed)
  195. if (channels.headers.hasSubscribers) {
  196. channels.headers.publish({ request: this, response: { statusCode, headers, statusText } })
  197. }
  198. try {
  199. return this[kHandler].onHeaders(statusCode, headers, resume, statusText)
  200. } catch (err) {
  201. this.abort(err)
  202. }
  203. }
  204. onData (chunk) {
  205. assert(!this.aborted)
  206. assert(!this.completed)
  207. try {
  208. return this[kHandler].onData(chunk)
  209. } catch (err) {
  210. this.abort(err)
  211. return false
  212. }
  213. }
  214. onUpgrade (statusCode, headers, socket) {
  215. assert(!this.aborted)
  216. assert(!this.completed)
  217. return this[kHandler].onUpgrade(statusCode, headers, socket)
  218. }
  219. onComplete (trailers) {
  220. this.onFinally()
  221. assert(!this.aborted)
  222. this.completed = true
  223. if (channels.trailers.hasSubscribers) {
  224. channels.trailers.publish({ request: this, trailers })
  225. }
  226. try {
  227. return this[kHandler].onComplete(trailers)
  228. } catch (err) {
  229. // TODO (fix): This might be a bad idea?
  230. this.onError(err)
  231. }
  232. }
  233. onError (error) {
  234. this.onFinally()
  235. if (channels.error.hasSubscribers) {
  236. channels.error.publish({ request: this, error })
  237. }
  238. if (this.aborted) {
  239. return
  240. }
  241. this.aborted = true
  242. return this[kHandler].onError(error)
  243. }
  244. onFinally () {
  245. if (this.errorHandler) {
  246. this.body.off('error', this.errorHandler)
  247. this.errorHandler = null
  248. }
  249. if (this.endHandler) {
  250. this.body.off('end', this.endHandler)
  251. this.endHandler = null
  252. }
  253. }
  254. addHeader (key, value) {
  255. processHeader(this, key, value)
  256. return this
  257. }
  258. }
  259. function processHeader (request, key, val) {
  260. if (val && (typeof val === 'object' && !Array.isArray(val))) {
  261. throw new InvalidArgumentError(`invalid ${key} header`)
  262. } else if (val === undefined) {
  263. return
  264. }
  265. let headerName = headerNameLowerCasedRecord[key]
  266. if (headerName === undefined) {
  267. headerName = key.toLowerCase()
  268. if (headerNameLowerCasedRecord[headerName] === undefined && !isValidHTTPToken(headerName)) {
  269. throw new InvalidArgumentError('invalid header key')
  270. }
  271. }
  272. if (Array.isArray(val)) {
  273. const arr = []
  274. for (let i = 0; i < val.length; i++) {
  275. if (typeof val[i] === 'string') {
  276. if (!isValidHeaderValue(val[i])) {
  277. throw new InvalidArgumentError(`invalid ${key} header`)
  278. }
  279. arr.push(val[i])
  280. } else if (val[i] === null) {
  281. arr.push('')
  282. } else if (typeof val[i] === 'object') {
  283. throw new InvalidArgumentError(`invalid ${key} header`)
  284. } else {
  285. arr.push(`${val[i]}`)
  286. }
  287. }
  288. val = arr
  289. } else if (typeof val === 'string') {
  290. if (!isValidHeaderValue(val)) {
  291. throw new InvalidArgumentError(`invalid ${key} header`)
  292. }
  293. } else if (val === null) {
  294. val = ''
  295. } else {
  296. val = `${val}`
  297. }
  298. if (request.host === null && headerName === 'host') {
  299. if (typeof val !== 'string') {
  300. throw new InvalidArgumentError('invalid host header')
  301. }
  302. // Consumed by Client
  303. request.host = val
  304. } else if (request.contentLength === null && headerName === 'content-length') {
  305. request.contentLength = parseInt(val, 10)
  306. if (!Number.isFinite(request.contentLength)) {
  307. throw new InvalidArgumentError('invalid content-length header')
  308. }
  309. } else if (request.contentType === null && headerName === 'content-type') {
  310. request.contentType = val
  311. request.headers.push(key, val)
  312. } else if (headerName === 'transfer-encoding' || headerName === 'keep-alive' || headerName === 'upgrade') {
  313. throw new InvalidArgumentError(`invalid ${headerName} header`)
  314. } else if (headerName === 'connection') {
  315. const value = typeof val === 'string' ? val.toLowerCase() : null
  316. if (value !== 'close' && value !== 'keep-alive') {
  317. throw new InvalidArgumentError('invalid connection header')
  318. }
  319. if (value === 'close') {
  320. request.reset = true
  321. }
  322. } else if (headerName === 'expect') {
  323. throw new NotSupportedError('expect header not supported')
  324. } else {
  325. request.headers.push(key, val)
  326. }
  327. }
  328. module.exports = Request