source: node_modules/undici/lib/core/request.js

main
Last change on this file was d24f17c, checked in by Aleksandar Panovski <apano77@…>, 15 months ago

Initial commit

  • Property mode set to 100644
File size: 13.6 KB
Line 
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
Note: See TracBrowser for help on using the repository browser.