request.js 11 KB

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