request.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. 'use strict'
  2. const {
  3. InvalidArgumentError,
  4. NotSupportedError
  5. } = require('./errors')
  6. const assert = require('assert')
  7. const util = require('./util')
  8. // tokenRegExp and headerCharRegex have been lifted from
  9. // https://github.com/nodejs/node/blob/main/lib/_http_common.js
  10. /**
  11. * Verifies that the given val is a valid HTTP token
  12. * per the rules defined in RFC 7230
  13. * See https://tools.ietf.org/html/rfc7230#section-3.2.6
  14. */
  15. const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/
  16. /**
  17. * Matches if val contains an invalid field-vchar
  18. * field-value = *( field-content / obs-fold )
  19. * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
  20. * field-vchar = VCHAR / obs-text
  21. */
  22. const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/
  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. const channels = {}
  27. let extractBody
  28. try {
  29. const diagnosticsChannel = require('diagnostics_channel')
  30. channels.create = diagnosticsChannel.channel('undici:request:create')
  31. channels.bodySent = diagnosticsChannel.channel('undici:request:bodySent')
  32. channels.headers = diagnosticsChannel.channel('undici:request:headers')
  33. channels.trailers = diagnosticsChannel.channel('undici:request:trailers')
  34. channels.error = diagnosticsChannel.channel('undici:request:error')
  35. } catch {
  36. channels.create = { hasSubscribers: false }
  37. channels.bodySent = { hasSubscribers: false }
  38. channels.headers = { hasSubscribers: false }
  39. channels.trailers = { hasSubscribers: false }
  40. channels.error = { hasSubscribers: false }
  41. }
  42. class Request {
  43. constructor (origin, {
  44. path,
  45. method,
  46. body,
  47. headers,
  48. query,
  49. idempotent,
  50. blocking,
  51. upgrade,
  52. headersTimeout,
  53. bodyTimeout,
  54. reset,
  55. throwOnError
  56. }, handler) {
  57. if (typeof path !== 'string') {
  58. throw new InvalidArgumentError('path must be a string')
  59. } else if (
  60. path[0] !== '/' &&
  61. !(path.startsWith('http://') || path.startsWith('https://')) &&
  62. method !== 'CONNECT'
  63. ) {
  64. throw new InvalidArgumentError('path must be an absolute URL or start with a slash')
  65. } else if (invalidPathRegex.exec(path) !== null) {
  66. throw new InvalidArgumentError('invalid request path')
  67. }
  68. if (typeof method !== 'string') {
  69. throw new InvalidArgumentError('method must be a string')
  70. } else if (tokenRegExp.exec(method) === null) {
  71. throw new InvalidArgumentError('invalid request method')
  72. }
  73. if (upgrade && typeof upgrade !== 'string') {
  74. throw new InvalidArgumentError('upgrade must be a string')
  75. }
  76. if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) {
  77. throw new InvalidArgumentError('invalid headersTimeout')
  78. }
  79. if (bodyTimeout != null && (!Number.isFinite(bodyTimeout) || bodyTimeout < 0)) {
  80. throw new InvalidArgumentError('invalid bodyTimeout')
  81. }
  82. if (reset != null && typeof reset !== 'boolean') {
  83. throw new InvalidArgumentError('invalid reset')
  84. }
  85. this.headersTimeout = headersTimeout
  86. this.bodyTimeout = bodyTimeout
  87. this.throwOnError = throwOnError === true
  88. this.method = method
  89. if (body == null) {
  90. this.body = null
  91. } else if (util.isStream(body)) {
  92. this.body = body
  93. } else if (util.isBuffer(body)) {
  94. this.body = body.byteLength ? body : null
  95. } else if (ArrayBuffer.isView(body)) {
  96. this.body = body.buffer.byteLength ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null
  97. } else if (body instanceof ArrayBuffer) {
  98. this.body = body.byteLength ? Buffer.from(body) : null
  99. } else if (typeof body === 'string') {
  100. this.body = body.length ? Buffer.from(body) : null
  101. } else if (util.isFormDataLike(body) || util.isIterable(body) || util.isBlobLike(body)) {
  102. this.body = body
  103. } else {
  104. throw new InvalidArgumentError('body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable')
  105. }
  106. this.completed = false
  107. this.aborted = false
  108. this.upgrade = upgrade || null
  109. this.path = query ? util.buildURL(path, query) : path
  110. this.origin = origin
  111. this.idempotent = idempotent == null
  112. ? method === 'HEAD' || method === 'GET'
  113. : idempotent
  114. this.blocking = blocking == null ? false : blocking
  115. this.reset = reset == null ? null : reset
  116. this.host = null
  117. this.contentLength = null
  118. this.contentType = null
  119. this.headers = ''
  120. if (Array.isArray(headers)) {
  121. if (headers.length % 2 !== 0) {
  122. throw new InvalidArgumentError('headers array must be even')
  123. }
  124. for (let i = 0; i < headers.length; i += 2) {
  125. processHeader(this, headers[i], headers[i + 1])
  126. }
  127. } else if (headers && typeof headers === 'object') {
  128. const keys = Object.keys(headers)
  129. for (let i = 0; i < keys.length; i++) {
  130. const key = keys[i]
  131. processHeader(this, key, headers[key])
  132. }
  133. } else if (headers != null) {
  134. throw new InvalidArgumentError('headers must be an object or an array')
  135. }
  136. if (util.isFormDataLike(this.body)) {
  137. if (util.nodeMajor < 16 || (util.nodeMajor === 16 && util.nodeMinor < 8)) {
  138. throw new InvalidArgumentError('Form-Data bodies are only supported in node v16.8 and newer.')
  139. }
  140. if (!extractBody) {
  141. extractBody = require('../fetch/body.js').extractBody
  142. }
  143. const [bodyStream, contentType] = extractBody(body)
  144. if (this.contentType == null) {
  145. this.contentType = contentType
  146. this.headers += `content-type: ${contentType}\r\n`
  147. }
  148. this.body = bodyStream.stream
  149. this.contentLength = bodyStream.length
  150. } else if (util.isBlobLike(body) && this.contentType == null && body.type) {
  151. this.contentType = body.type
  152. this.headers += `content-type: ${body.type}\r\n`
  153. }
  154. util.validateHandler(handler, method, upgrade)
  155. this.servername = util.getServerName(this.host)
  156. this[kHandler] = handler
  157. if (channels.create.hasSubscribers) {
  158. channels.create.publish({ request: this })
  159. }
  160. }
  161. onBodySent (chunk) {
  162. if (this[kHandler].onBodySent) {
  163. try {
  164. this[kHandler].onBodySent(chunk)
  165. } catch (err) {
  166. this.onError(err)
  167. }
  168. }
  169. }
  170. onRequestSent () {
  171. if (channels.bodySent.hasSubscribers) {
  172. channels.bodySent.publish({ request: this })
  173. }
  174. }
  175. onConnect (abort) {
  176. assert(!this.aborted)
  177. assert(!this.completed)
  178. return this[kHandler].onConnect(abort)
  179. }
  180. onHeaders (statusCode, headers, resume, statusText) {
  181. assert(!this.aborted)
  182. assert(!this.completed)
  183. if (channels.headers.hasSubscribers) {
  184. channels.headers.publish({ request: this, response: { statusCode, headers, statusText } })
  185. }
  186. return this[kHandler].onHeaders(statusCode, headers, resume, statusText)
  187. }
  188. onData (chunk) {
  189. assert(!this.aborted)
  190. assert(!this.completed)
  191. return this[kHandler].onData(chunk)
  192. }
  193. onUpgrade (statusCode, headers, socket) {
  194. assert(!this.aborted)
  195. assert(!this.completed)
  196. return this[kHandler].onUpgrade(statusCode, headers, socket)
  197. }
  198. onComplete (trailers) {
  199. assert(!this.aborted)
  200. this.completed = true
  201. if (channels.trailers.hasSubscribers) {
  202. channels.trailers.publish({ request: this, trailers })
  203. }
  204. return this[kHandler].onComplete(trailers)
  205. }
  206. onError (error) {
  207. if (channels.error.hasSubscribers) {
  208. channels.error.publish({ request: this, error })
  209. }
  210. if (this.aborted) {
  211. return
  212. }
  213. this.aborted = true
  214. return this[kHandler].onError(error)
  215. }
  216. addHeader (key, value) {
  217. processHeader(this, key, value)
  218. return this
  219. }
  220. }
  221. function processHeaderValue (key, val) {
  222. if (val && typeof val === 'object') {
  223. throw new InvalidArgumentError(`invalid ${key} header`)
  224. }
  225. val = val != null ? `${val}` : ''
  226. if (headerCharRegex.exec(val) !== null) {
  227. throw new InvalidArgumentError(`invalid ${key} header`)
  228. }
  229. return `${key}: ${val}\r\n`
  230. }
  231. function processHeader (request, key, val) {
  232. if (val && (typeof val === 'object' && !Array.isArray(val))) {
  233. throw new InvalidArgumentError(`invalid ${key} header`)
  234. } else if (val === undefined) {
  235. return
  236. }
  237. if (
  238. request.host === null &&
  239. key.length === 4 &&
  240. key.toLowerCase() === 'host'
  241. ) {
  242. if (headerCharRegex.exec(val) !== null) {
  243. throw new InvalidArgumentError(`invalid ${key} header`)
  244. }
  245. // Consumed by Client
  246. request.host = val
  247. } else if (
  248. request.contentLength === null &&
  249. key.length === 14 &&
  250. key.toLowerCase() === 'content-length'
  251. ) {
  252. request.contentLength = parseInt(val, 10)
  253. if (!Number.isFinite(request.contentLength)) {
  254. throw new InvalidArgumentError('invalid content-length header')
  255. }
  256. } else if (
  257. request.contentType === null &&
  258. key.length === 12 &&
  259. key.toLowerCase() === 'content-type'
  260. ) {
  261. request.contentType = val
  262. request.headers += processHeaderValue(key, val)
  263. } else if (
  264. key.length === 17 &&
  265. key.toLowerCase() === 'transfer-encoding'
  266. ) {
  267. throw new InvalidArgumentError('invalid transfer-encoding header')
  268. } else if (
  269. key.length === 10 &&
  270. key.toLowerCase() === 'connection'
  271. ) {
  272. const value = typeof val === 'string' ? val.toLowerCase() : null
  273. if (value !== 'close' && value !== 'keep-alive') {
  274. throw new InvalidArgumentError('invalid connection header')
  275. } else if (value === 'close') {
  276. request.reset = true
  277. }
  278. } else if (
  279. key.length === 10 &&
  280. key.toLowerCase() === 'keep-alive'
  281. ) {
  282. throw new InvalidArgumentError('invalid keep-alive header')
  283. } else if (
  284. key.length === 7 &&
  285. key.toLowerCase() === 'upgrade'
  286. ) {
  287. throw new InvalidArgumentError('invalid upgrade header')
  288. } else if (
  289. key.length === 6 &&
  290. key.toLowerCase() === 'expect'
  291. ) {
  292. throw new NotSupportedError('expect header not supported')
  293. } else if (tokenRegExp.exec(key) === null) {
  294. throw new InvalidArgumentError('invalid header key')
  295. } else {
  296. if (Array.isArray(val)) {
  297. for (let i = 0; i < val.length; i++) {
  298. request.headers += processHeaderValue(key, val[i])
  299. }
  300. } else {
  301. request.headers += processHeaderValue(key, val)
  302. }
  303. }
  304. }
  305. module.exports = Request