• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict'
2
3const {
4  InvalidArgumentError,
5  NotSupportedError
6} = require('./errors')
7const assert = require('assert')
8const { kHTTP2BuildRequest, kHTTP2CopyHeaders, kHTTP1BuildRequest } = require('./symbols')
9const util = require('./util')
10
11// tokenRegExp and headerCharRegex have been lifted from
12// https://github.com/nodejs/node/blob/main/lib/_http_common.js
13
14/**
15 * Verifies that the given val is a valid HTTP token
16 * per the rules defined in RFC 7230
17 * See https://tools.ietf.org/html/rfc7230#section-3.2.6
18 */
19const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/
20
21/**
22 * Matches if val contains an invalid field-vchar
23 *  field-value    = *( field-content / obs-fold )
24 *  field-content  = field-vchar [ 1*( SP / HTAB ) field-vchar ]
25 *  field-vchar    = VCHAR / obs-text
26 */
27const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/
28
29// Verifies that a given path is valid does not contain control chars \x00 to \x20
30const invalidPathRegex = /[^\u0021-\u00ff]/
31
32const kHandler = Symbol('handler')
33
34const channels = {}
35
36let extractBody
37
38try {
39  const diagnosticsChannel = require('diagnostics_channel')
40  channels.create = diagnosticsChannel.channel('undici:request:create')
41  channels.bodySent = diagnosticsChannel.channel('undici:request:bodySent')
42  channels.headers = diagnosticsChannel.channel('undici:request:headers')
43  channels.trailers = diagnosticsChannel.channel('undici:request:trailers')
44  channels.error = diagnosticsChannel.channel('undici:request:error')
45} catch {
46  channels.create = { hasSubscribers: false }
47  channels.bodySent = { hasSubscribers: false }
48  channels.headers = { hasSubscribers: false }
49  channels.trailers = { hasSubscribers: false }
50  channels.error = { hasSubscribers: false }
51}
52
53class Request {
54  constructor (origin, {
55    path,
56    method,
57    body,
58    headers,
59    query,
60    idempotent,
61    blocking,
62    upgrade,
63    headersTimeout,
64    bodyTimeout,
65    reset,
66    throwOnError,
67    expectContinue
68  }, handler) {
69    if (typeof path !== 'string') {
70      throw new InvalidArgumentError('path must be a string')
71    } else if (
72      path[0] !== '/' &&
73      !(path.startsWith('http://') || path.startsWith('https://')) &&
74      method !== 'CONNECT'
75    ) {
76      throw new InvalidArgumentError('path must be an absolute URL or start with a slash')
77    } else if (invalidPathRegex.exec(path) !== null) {
78      throw new InvalidArgumentError('invalid request path')
79    }
80
81    if (typeof method !== 'string') {
82      throw new InvalidArgumentError('method must be a string')
83    } else if (tokenRegExp.exec(method) === null) {
84      throw new InvalidArgumentError('invalid request method')
85    }
86
87    if (upgrade && typeof upgrade !== 'string') {
88      throw new InvalidArgumentError('upgrade must be a string')
89    }
90
91    if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) {
92      throw new InvalidArgumentError('invalid headersTimeout')
93    }
94
95    if (bodyTimeout != null && (!Number.isFinite(bodyTimeout) || bodyTimeout < 0)) {
96      throw new InvalidArgumentError('invalid bodyTimeout')
97    }
98
99    if (reset != null && typeof reset !== 'boolean') {
100      throw new InvalidArgumentError('invalid reset')
101    }
102
103    if (expectContinue != null && typeof expectContinue !== 'boolean') {
104      throw new InvalidArgumentError('invalid expectContinue')
105    }
106
107    this.headersTimeout = headersTimeout
108
109    this.bodyTimeout = bodyTimeout
110
111    this.throwOnError = throwOnError === true
112
113    this.method = method
114
115    this.abort = null
116
117    if (body == null) {
118      this.body = null
119    } else if (util.isStream(body)) {
120      this.body = body
121
122      const rState = this.body._readableState
123      if (!rState || !rState.autoDestroy) {
124        this.endHandler = function autoDestroy () {
125          util.destroy(this)
126        }
127        this.body.on('end', this.endHandler)
128      }
129
130      this.errorHandler = err => {
131        if (this.abort) {
132          this.abort(err)
133        } else {
134          this.error = err
135        }
136      }
137      this.body.on('error', this.errorHandler)
138    } else if (util.isBuffer(body)) {
139      this.body = body.byteLength ? body : null
140    } else if (ArrayBuffer.isView(body)) {
141      this.body = body.buffer.byteLength ? Buffer.from(body.buffer, body.byteOffset, body.byteLength) : null
142    } else if (body instanceof ArrayBuffer) {
143      this.body = body.byteLength ? Buffer.from(body) : null
144    } else if (typeof body === 'string') {
145      this.body = body.length ? Buffer.from(body) : null
146    } else if (util.isFormDataLike(body) || util.isIterable(body) || util.isBlobLike(body)) {
147      this.body = body
148    } else {
149      throw new InvalidArgumentError('body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable')
150    }
151
152    this.completed = false
153
154    this.aborted = false
155
156    this.upgrade = upgrade || null
157
158    this.path = query ? util.buildURL(path, query) : path
159
160    this.origin = origin
161
162    this.idempotent = idempotent == null
163      ? method === 'HEAD' || method === 'GET'
164      : idempotent
165
166    this.blocking = blocking == null ? false : blocking
167
168    this.reset = reset == null ? null : reset
169
170    this.host = null
171
172    this.contentLength = null
173
174    this.contentType = null
175
176    this.headers = ''
177
178    // Only for H2
179    this.expectContinue = expectContinue != null ? expectContinue : false
180
181    if (Array.isArray(headers)) {
182      if (headers.length % 2 !== 0) {
183        throw new InvalidArgumentError('headers array must be even')
184      }
185      for (let i = 0; i < headers.length; i += 2) {
186        processHeader(this, headers[i], headers[i + 1])
187      }
188    } else if (headers && typeof headers === 'object') {
189      const keys = Object.keys(headers)
190      for (let i = 0; i < keys.length; i++) {
191        const key = keys[i]
192        processHeader(this, key, headers[key])
193      }
194    } else if (headers != null) {
195      throw new InvalidArgumentError('headers must be an object or an array')
196    }
197
198    if (util.isFormDataLike(this.body)) {
199      if (util.nodeMajor < 16 || (util.nodeMajor === 16 && util.nodeMinor < 8)) {
200        throw new InvalidArgumentError('Form-Data bodies are only supported in node v16.8 and newer.')
201      }
202
203      if (!extractBody) {
204        extractBody = require('../fetch/body.js').extractBody
205      }
206
207      const [bodyStream, contentType] = extractBody(body)
208      if (this.contentType == null) {
209        this.contentType = contentType
210        this.headers += `content-type: ${contentType}\r\n`
211      }
212      this.body = bodyStream.stream
213      this.contentLength = bodyStream.length
214    } else if (util.isBlobLike(body) && this.contentType == null && body.type) {
215      this.contentType = body.type
216      this.headers += `content-type: ${body.type}\r\n`
217    }
218
219    util.validateHandler(handler, method, upgrade)
220
221    this.servername = util.getServerName(this.host)
222
223    this[kHandler] = handler
224
225    if (channels.create.hasSubscribers) {
226      channels.create.publish({ request: this })
227    }
228  }
229
230  onBodySent (chunk) {
231    if (this[kHandler].onBodySent) {
232      try {
233        return this[kHandler].onBodySent(chunk)
234      } catch (err) {
235        this.abort(err)
236      }
237    }
238  }
239
240  onRequestSent () {
241    if (channels.bodySent.hasSubscribers) {
242      channels.bodySent.publish({ request: this })
243    }
244
245    if (this[kHandler].onRequestSent) {
246      try {
247        return this[kHandler].onRequestSent()
248      } catch (err) {
249        this.abort(err)
250      }
251    }
252  }
253
254  onConnect (abort) {
255    assert(!this.aborted)
256    assert(!this.completed)
257
258    if (this.error) {
259      abort(this.error)
260    } else {
261      this.abort = abort
262      return this[kHandler].onConnect(abort)
263    }
264  }
265
266  onHeaders (statusCode, headers, resume, statusText) {
267    assert(!this.aborted)
268    assert(!this.completed)
269
270    if (channels.headers.hasSubscribers) {
271      channels.headers.publish({ request: this, response: { statusCode, headers, statusText } })
272    }
273
274    try {
275      return this[kHandler].onHeaders(statusCode, headers, resume, statusText)
276    } catch (err) {
277      this.abort(err)
278    }
279  }
280
281  onData (chunk) {
282    assert(!this.aborted)
283    assert(!this.completed)
284
285    try {
286      return this[kHandler].onData(chunk)
287    } catch (err) {
288      this.abort(err)
289      return false
290    }
291  }
292
293  onUpgrade (statusCode, headers, socket) {
294    assert(!this.aborted)
295    assert(!this.completed)
296
297    return this[kHandler].onUpgrade(statusCode, headers, socket)
298  }
299
300  onComplete (trailers) {
301    this.onFinally()
302
303    assert(!this.aborted)
304
305    this.completed = true
306    if (channels.trailers.hasSubscribers) {
307      channels.trailers.publish({ request: this, trailers })
308    }
309
310    try {
311      return this[kHandler].onComplete(trailers)
312    } catch (err) {
313      // TODO (fix): This might be a bad idea?
314      this.onError(err)
315    }
316  }
317
318  onError (error) {
319    this.onFinally()
320
321    if (channels.error.hasSubscribers) {
322      channels.error.publish({ request: this, error })
323    }
324
325    if (this.aborted) {
326      return
327    }
328    this.aborted = true
329
330    return this[kHandler].onError(error)
331  }
332
333  onFinally () {
334    if (this.errorHandler) {
335      this.body.off('error', this.errorHandler)
336      this.errorHandler = null
337    }
338
339    if (this.endHandler) {
340      this.body.off('end', this.endHandler)
341      this.endHandler = null
342    }
343  }
344
345  // TODO: adjust to support H2
346  addHeader (key, value) {
347    processHeader(this, key, value)
348    return this
349  }
350
351  static [kHTTP1BuildRequest] (origin, opts, handler) {
352    // TODO: Migrate header parsing here, to make Requests
353    // HTTP agnostic
354    return new Request(origin, opts, handler)
355  }
356
357  static [kHTTP2BuildRequest] (origin, opts, handler) {
358    const headers = opts.headers
359    opts = { ...opts, headers: null }
360
361    const request = new Request(origin, opts, handler)
362
363    request.headers = {}
364
365    if (Array.isArray(headers)) {
366      if (headers.length % 2 !== 0) {
367        throw new InvalidArgumentError('headers array must be even')
368      }
369      for (let i = 0; i < headers.length; i += 2) {
370        processHeader(request, headers[i], headers[i + 1], true)
371      }
372    } else if (headers && typeof headers === 'object') {
373      const keys = Object.keys(headers)
374      for (let i = 0; i < keys.length; i++) {
375        const key = keys[i]
376        processHeader(request, key, headers[key], true)
377      }
378    } else if (headers != null) {
379      throw new InvalidArgumentError('headers must be an object or an array')
380    }
381
382    return request
383  }
384
385  static [kHTTP2CopyHeaders] (raw) {
386    const rawHeaders = raw.split('\r\n')
387    const headers = {}
388
389    for (const header of rawHeaders) {
390      const [key, value] = header.split(': ')
391
392      if (value == null || value.length === 0) continue
393
394      if (headers[key]) headers[key] += `,${value}`
395      else headers[key] = value
396    }
397
398    return headers
399  }
400}
401
402function processHeaderValue (key, val, skipAppend) {
403  if (val && typeof val === 'object') {
404    throw new InvalidArgumentError(`invalid ${key} header`)
405  }
406
407  val = val != null ? `${val}` : ''
408
409  if (headerCharRegex.exec(val) !== null) {
410    throw new InvalidArgumentError(`invalid ${key} header`)
411  }
412
413  return skipAppend ? val : `${key}: ${val}\r\n`
414}
415
416function processHeader (request, key, val, skipAppend = false) {
417  if (val && (typeof val === 'object' && !Array.isArray(val))) {
418    throw new InvalidArgumentError(`invalid ${key} header`)
419  } else if (val === undefined) {
420    return
421  }
422
423  if (
424    request.host === null &&
425    key.length === 4 &&
426    key.toLowerCase() === 'host'
427  ) {
428    if (headerCharRegex.exec(val) !== null) {
429      throw new InvalidArgumentError(`invalid ${key} header`)
430    }
431    // Consumed by Client
432    request.host = val
433  } else if (
434    request.contentLength === null &&
435    key.length === 14 &&
436    key.toLowerCase() === 'content-length'
437  ) {
438    request.contentLength = parseInt(val, 10)
439    if (!Number.isFinite(request.contentLength)) {
440      throw new InvalidArgumentError('invalid content-length header')
441    }
442  } else if (
443    request.contentType === null &&
444    key.length === 12 &&
445    key.toLowerCase() === 'content-type'
446  ) {
447    request.contentType = val
448    if (skipAppend) request.headers[key] = processHeaderValue(key, val, skipAppend)
449    else request.headers += processHeaderValue(key, val)
450  } else if (
451    key.length === 17 &&
452    key.toLowerCase() === 'transfer-encoding'
453  ) {
454    throw new InvalidArgumentError('invalid transfer-encoding header')
455  } else if (
456    key.length === 10 &&
457    key.toLowerCase() === 'connection'
458  ) {
459    const value = typeof val === 'string' ? val.toLowerCase() : null
460    if (value !== 'close' && value !== 'keep-alive') {
461      throw new InvalidArgumentError('invalid connection header')
462    } else if (value === 'close') {
463      request.reset = true
464    }
465  } else if (
466    key.length === 10 &&
467    key.toLowerCase() === 'keep-alive'
468  ) {
469    throw new InvalidArgumentError('invalid keep-alive header')
470  } else if (
471    key.length === 7 &&
472    key.toLowerCase() === 'upgrade'
473  ) {
474    throw new InvalidArgumentError('invalid upgrade header')
475  } else if (
476    key.length === 6 &&
477    key.toLowerCase() === 'expect'
478  ) {
479    throw new NotSupportedError('expect header not supported')
480  } else if (tokenRegExp.exec(key) === null) {
481    throw new InvalidArgumentError('invalid header key')
482  } else {
483    if (Array.isArray(val)) {
484      for (let i = 0; i < val.length; i++) {
485        if (skipAppend) {
486          if (request.headers[key]) request.headers[key] += `,${processHeaderValue(key, val[i], skipAppend)}`
487          else request.headers[key] = processHeaderValue(key, val[i], skipAppend)
488        } else {
489          request.headers += processHeaderValue(key, val[i])
490        }
491      }
492    } else {
493      if (skipAppend) request.headers[key] = processHeaderValue(key, val, skipAppend)
494      else request.headers += processHeaderValue(key, val)
495    }
496  }
497}
498
499module.exports = Request
500