source: node_modules/undici/lib/core/util.js@ 65b6638

main
Last change on this file since 65b6638 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 assert = require('assert')
4const { kDestroyed, kBodyUsed } = require('./symbols')
5const { IncomingMessage } = require('http')
6const stream = require('stream')
7const net = require('net')
8const { InvalidArgumentError } = require('./errors')
9const { Blob } = require('buffer')
10const nodeUtil = require('util')
11const { stringify } = require('querystring')
12
13const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(v => Number(v))
14
15function nop () {}
16
17function isStream (obj) {
18 return obj && typeof obj === 'object' && typeof obj.pipe === 'function' && typeof obj.on === 'function'
19}
20
21// based on https://github.com/node-fetch/fetch-blob/blob/8ab587d34080de94140b54f07168451e7d0b655e/index.js#L229-L241 (MIT License)
22function isBlobLike (object) {
23 return (Blob && object instanceof Blob) || (
24 object &&
25 typeof object === 'object' &&
26 (typeof object.stream === 'function' ||
27 typeof object.arrayBuffer === 'function') &&
28 /^(Blob|File)$/.test(object[Symbol.toStringTag])
29 )
30}
31
32function buildURL (url, queryParams) {
33 if (url.includes('?') || url.includes('#')) {
34 throw new Error('Query params cannot be passed when url already contains "?" or "#".')
35 }
36
37 const stringified = stringify(queryParams)
38
39 if (stringified) {
40 url += '?' + stringified
41 }
42
43 return url
44}
45
46function parseURL (url) {
47 if (typeof url === 'string') {
48 url = new URL(url)
49
50 if (!/^https?:/.test(url.origin || url.protocol)) {
51 throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.')
52 }
53
54 return url
55 }
56
57 if (!url || typeof url !== 'object') {
58 throw new InvalidArgumentError('Invalid URL: The URL argument must be a non-null object.')
59 }
60
61 if (!/^https?:/.test(url.origin || url.protocol)) {
62 throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.')
63 }
64
65 if (!(url instanceof URL)) {
66 if (url.port != null && url.port !== '' && !Number.isFinite(parseInt(url.port))) {
67 throw new InvalidArgumentError('Invalid URL: port must be a valid integer or a string representation of an integer.')
68 }
69
70 if (url.path != null && typeof url.path !== 'string') {
71 throw new InvalidArgumentError('Invalid URL path: the path must be a string or null/undefined.')
72 }
73
74 if (url.pathname != null && typeof url.pathname !== 'string') {
75 throw new InvalidArgumentError('Invalid URL pathname: the pathname must be a string or null/undefined.')
76 }
77
78 if (url.hostname != null && typeof url.hostname !== 'string') {
79 throw new InvalidArgumentError('Invalid URL hostname: the hostname must be a string or null/undefined.')
80 }
81
82 if (url.origin != null && typeof url.origin !== 'string') {
83 throw new InvalidArgumentError('Invalid URL origin: the origin must be a string or null/undefined.')
84 }
85
86 const port = url.port != null
87 ? url.port
88 : (url.protocol === 'https:' ? 443 : 80)
89 let origin = url.origin != null
90 ? url.origin
91 : `${url.protocol}//${url.hostname}:${port}`
92 let path = url.path != null
93 ? url.path
94 : `${url.pathname || ''}${url.search || ''}`
95
96 if (origin.endsWith('/')) {
97 origin = origin.substring(0, origin.length - 1)
98 }
99
100 if (path && !path.startsWith('/')) {
101 path = `/${path}`
102 }
103 // new URL(path, origin) is unsafe when `path` contains an absolute URL
104 // From https://developer.mozilla.org/en-US/docs/Web/API/URL/URL:
105 // If first parameter is a relative URL, second param is required, and will be used as the base URL.
106 // If first parameter is an absolute URL, a given second param will be ignored.
107 url = new URL(origin + path)
108 }
109
110 return url
111}
112
113function parseOrigin (url) {
114 url = parseURL(url)
115
116 if (url.pathname !== '/' || url.search || url.hash) {
117 throw new InvalidArgumentError('invalid url')
118 }
119
120 return url
121}
122
123function getHostname (host) {
124 if (host[0] === '[') {
125 const idx = host.indexOf(']')
126
127 assert(idx !== -1)
128 return host.substring(1, idx)
129 }
130
131 const idx = host.indexOf(':')
132 if (idx === -1) return host
133
134 return host.substring(0, idx)
135}
136
137// IP addresses are not valid server names per RFC6066
138// > Currently, the only server names supported are DNS hostnames
139function getServerName (host) {
140 if (!host) {
141 return null
142 }
143
144 assert.strictEqual(typeof host, 'string')
145
146 const servername = getHostname(host)
147 if (net.isIP(servername)) {
148 return ''
149 }
150
151 return servername
152}
153
154function deepClone (obj) {
155 return JSON.parse(JSON.stringify(obj))
156}
157
158function isAsyncIterable (obj) {
159 return !!(obj != null && typeof obj[Symbol.asyncIterator] === 'function')
160}
161
162function isIterable (obj) {
163 return !!(obj != null && (typeof obj[Symbol.iterator] === 'function' || typeof obj[Symbol.asyncIterator] === 'function'))
164}
165
166function bodyLength (body) {
167 if (body == null) {
168 return 0
169 } else if (isStream(body)) {
170 const state = body._readableState
171 return state && state.objectMode === false && state.ended === true && Number.isFinite(state.length)
172 ? state.length
173 : null
174 } else if (isBlobLike(body)) {
175 return body.size != null ? body.size : null
176 } else if (isBuffer(body)) {
177 return body.byteLength
178 }
179
180 return null
181}
182
183function isDestroyed (stream) {
184 return !stream || !!(stream.destroyed || stream[kDestroyed])
185}
186
187function isReadableAborted (stream) {
188 const state = stream && stream._readableState
189 return isDestroyed(stream) && state && !state.endEmitted
190}
191
192function destroy (stream, err) {
193 if (stream == null || !isStream(stream) || isDestroyed(stream)) {
194 return
195 }
196
197 if (typeof stream.destroy === 'function') {
198 if (Object.getPrototypeOf(stream).constructor === IncomingMessage) {
199 // See: https://github.com/nodejs/node/pull/38505/files
200 stream.socket = null
201 }
202
203 stream.destroy(err)
204 } else if (err) {
205 process.nextTick((stream, err) => {
206 stream.emit('error', err)
207 }, stream, err)
208 }
209
210 if (stream.destroyed !== true) {
211 stream[kDestroyed] = true
212 }
213}
214
215const KEEPALIVE_TIMEOUT_EXPR = /timeout=(\d+)/
216function parseKeepAliveTimeout (val) {
217 const m = val.toString().match(KEEPALIVE_TIMEOUT_EXPR)
218 return m ? parseInt(m[1], 10) * 1000 : null
219}
220
221function parseHeaders (headers, obj = {}) {
222 // For H2 support
223 if (!Array.isArray(headers)) return headers
224
225 for (let i = 0; i < headers.length; i += 2) {
226 const key = headers[i].toString().toLowerCase()
227 let val = obj[key]
228
229 if (!val) {
230 if (Array.isArray(headers[i + 1])) {
231 obj[key] = headers[i + 1].map(x => x.toString('utf8'))
232 } else {
233 obj[key] = headers[i + 1].toString('utf8')
234 }
235 } else {
236 if (!Array.isArray(val)) {
237 val = [val]
238 obj[key] = val
239 }
240 val.push(headers[i + 1].toString('utf8'))
241 }
242 }
243
244 // See https://github.com/nodejs/node/pull/46528
245 if ('content-length' in obj && 'content-disposition' in obj) {
246 obj['content-disposition'] = Buffer.from(obj['content-disposition']).toString('latin1')
247 }
248
249 return obj
250}
251
252function parseRawHeaders (headers) {
253 const ret = []
254 let hasContentLength = false
255 let contentDispositionIdx = -1
256
257 for (let n = 0; n < headers.length; n += 2) {
258 const key = headers[n + 0].toString()
259 const val = headers[n + 1].toString('utf8')
260
261 if (key.length === 14 && (key === 'content-length' || key.toLowerCase() === 'content-length')) {
262 ret.push(key, val)
263 hasContentLength = true
264 } else if (key.length === 19 && (key === 'content-disposition' || key.toLowerCase() === 'content-disposition')) {
265 contentDispositionIdx = ret.push(key, val) - 1
266 } else {
267 ret.push(key, val)
268 }
269 }
270
271 // See https://github.com/nodejs/node/pull/46528
272 if (hasContentLength && contentDispositionIdx !== -1) {
273 ret[contentDispositionIdx] = Buffer.from(ret[contentDispositionIdx]).toString('latin1')
274 }
275
276 return ret
277}
278
279function isBuffer (buffer) {
280 // See, https://github.com/mcollina/undici/pull/319
281 return buffer instanceof Uint8Array || Buffer.isBuffer(buffer)
282}
283
284function validateHandler (handler, method, upgrade) {
285 if (!handler || typeof handler !== 'object') {
286 throw new InvalidArgumentError('handler must be an object')
287 }
288
289 if (typeof handler.onConnect !== 'function') {
290 throw new InvalidArgumentError('invalid onConnect method')
291 }
292
293 if (typeof handler.onError !== 'function') {
294 throw new InvalidArgumentError('invalid onError method')
295 }
296
297 if (typeof handler.onBodySent !== 'function' && handler.onBodySent !== undefined) {
298 throw new InvalidArgumentError('invalid onBodySent method')
299 }
300
301 if (upgrade || method === 'CONNECT') {
302 if (typeof handler.onUpgrade !== 'function') {
303 throw new InvalidArgumentError('invalid onUpgrade method')
304 }
305 } else {
306 if (typeof handler.onHeaders !== 'function') {
307 throw new InvalidArgumentError('invalid onHeaders method')
308 }
309
310 if (typeof handler.onData !== 'function') {
311 throw new InvalidArgumentError('invalid onData method')
312 }
313
314 if (typeof handler.onComplete !== 'function') {
315 throw new InvalidArgumentError('invalid onComplete method')
316 }
317 }
318}
319
320// A body is disturbed if it has been read from and it cannot
321// be re-used without losing state or data.
322function isDisturbed (body) {
323 return !!(body && (
324 stream.isDisturbed
325 ? stream.isDisturbed(body) || body[kBodyUsed] // TODO (fix): Why is body[kBodyUsed] needed?
326 : body[kBodyUsed] ||
327 body.readableDidRead ||
328 (body._readableState && body._readableState.dataEmitted) ||
329 isReadableAborted(body)
330 ))
331}
332
333function isErrored (body) {
334 return !!(body && (
335 stream.isErrored
336 ? stream.isErrored(body)
337 : /state: 'errored'/.test(nodeUtil.inspect(body)
338 )))
339}
340
341function isReadable (body) {
342 return !!(body && (
343 stream.isReadable
344 ? stream.isReadable(body)
345 : /state: 'readable'/.test(nodeUtil.inspect(body)
346 )))
347}
348
349function getSocketInfo (socket) {
350 return {
351 localAddress: socket.localAddress,
352 localPort: socket.localPort,
353 remoteAddress: socket.remoteAddress,
354 remotePort: socket.remotePort,
355 remoteFamily: socket.remoteFamily,
356 timeout: socket.timeout,
357 bytesWritten: socket.bytesWritten,
358 bytesRead: socket.bytesRead
359 }
360}
361
362async function * convertIterableToBuffer (iterable) {
363 for await (const chunk of iterable) {
364 yield Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
365 }
366}
367
368let ReadableStream
369function ReadableStreamFrom (iterable) {
370 if (!ReadableStream) {
371 ReadableStream = require('stream/web').ReadableStream
372 }
373
374 if (ReadableStream.from) {
375 return ReadableStream.from(convertIterableToBuffer(iterable))
376 }
377
378 let iterator
379 return new ReadableStream(
380 {
381 async start () {
382 iterator = iterable[Symbol.asyncIterator]()
383 },
384 async pull (controller) {
385 const { done, value } = await iterator.next()
386 if (done) {
387 queueMicrotask(() => {
388 controller.close()
389 })
390 } else {
391 const buf = Buffer.isBuffer(value) ? value : Buffer.from(value)
392 controller.enqueue(new Uint8Array(buf))
393 }
394 return controller.desiredSize > 0
395 },
396 async cancel (reason) {
397 await iterator.return()
398 }
399 },
400 0
401 )
402}
403
404// The chunk should be a FormData instance and contains
405// all the required methods.
406function isFormDataLike (object) {
407 return (
408 object &&
409 typeof object === 'object' &&
410 typeof object.append === 'function' &&
411 typeof object.delete === 'function' &&
412 typeof object.get === 'function' &&
413 typeof object.getAll === 'function' &&
414 typeof object.has === 'function' &&
415 typeof object.set === 'function' &&
416 object[Symbol.toStringTag] === 'FormData'
417 )
418}
419
420function throwIfAborted (signal) {
421 if (!signal) { return }
422 if (typeof signal.throwIfAborted === 'function') {
423 signal.throwIfAborted()
424 } else {
425 if (signal.aborted) {
426 // DOMException not available < v17.0.0
427 const err = new Error('The operation was aborted')
428 err.name = 'AbortError'
429 throw err
430 }
431 }
432}
433
434function addAbortListener (signal, listener) {
435 if ('addEventListener' in signal) {
436 signal.addEventListener('abort', listener, { once: true })
437 return () => signal.removeEventListener('abort', listener)
438 }
439 signal.addListener('abort', listener)
440 return () => signal.removeListener('abort', listener)
441}
442
443const hasToWellFormed = !!String.prototype.toWellFormed
444
445/**
446 * @param {string} val
447 */
448function toUSVString (val) {
449 if (hasToWellFormed) {
450 return `${val}`.toWellFormed()
451 } else if (nodeUtil.toUSVString) {
452 return nodeUtil.toUSVString(val)
453 }
454
455 return `${val}`
456}
457
458// Parsed accordingly to RFC 9110
459// https://www.rfc-editor.org/rfc/rfc9110#field.content-range
460function parseRangeHeader (range) {
461 if (range == null || range === '') return { start: 0, end: null, size: null }
462
463 const m = range ? range.match(/^bytes (\d+)-(\d+)\/(\d+)?$/) : null
464 return m
465 ? {
466 start: parseInt(m[1]),
467 end: m[2] ? parseInt(m[2]) : null,
468 size: m[3] ? parseInt(m[3]) : null
469 }
470 : null
471}
472
473const kEnumerableProperty = Object.create(null)
474kEnumerableProperty.enumerable = true
475
476module.exports = {
477 kEnumerableProperty,
478 nop,
479 isDisturbed,
480 isErrored,
481 isReadable,
482 toUSVString,
483 isReadableAborted,
484 isBlobLike,
485 parseOrigin,
486 parseURL,
487 getServerName,
488 isStream,
489 isIterable,
490 isAsyncIterable,
491 isDestroyed,
492 parseRawHeaders,
493 parseHeaders,
494 parseKeepAliveTimeout,
495 destroy,
496 bodyLength,
497 deepClone,
498 ReadableStreamFrom,
499 isBuffer,
500 validateHandler,
501 getSocketInfo,
502 isFormDataLike,
503 buildURL,
504 throwIfAborted,
505 addAbortListener,
506 parseRangeHeader,
507 nodeMajor,
508 nodeMinor,
509 nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13),
510 safeHTTPMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE']
511}
Note: See TracBrowser for help on using the repository browser.