undici
Advanced tools
Comparing version 6.13.0 to 6.14.0
# EventSource | ||
> ⚠️ Warning: the EventSource API is experimental. | ||
Undici exposes a WHATWG spec-compliant implementation of [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) | ||
@@ -14,4 +16,4 @@ for [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events). | ||
const evenSource = new EventSource('http://localhost:3000') | ||
evenSource.onmessage = (event) => { | ||
const eventSource = new EventSource('http://localhost:3000') | ||
eventSource.onmessage = (event) => { | ||
console.log(event.data) | ||
@@ -21,3 +23,25 @@ } | ||
## Using a custom Dispatcher | ||
undici allows you to set your own Dispatcher in the EventSource constructor. | ||
An example which allows you to modify the request headers is: | ||
```mjs | ||
import { EventSource, Agent } from 'undici' | ||
class CustomHeaderAgent extends Agent { | ||
dispatch (opts) { | ||
opts.headers['x-custom-header'] = 'hello world' | ||
return super.dispatch(...arguments) | ||
} | ||
} | ||
const eventSource = new EventSource('http://localhost:3000', { | ||
dispatcher: new CustomHeaderAgent() | ||
}) | ||
``` | ||
More information about the EventSource API can be found on | ||
[MDN](https://developer.mozilla.org/en-US/docs/Web/API/EventSource). | ||
[MDN](https://developer.mozilla.org/en-US/docs/Web/API/EventSource). |
@@ -7,8 +7,2 @@ # Fetch | ||
## File | ||
This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/File) | ||
In Node versions v18.13.0 and above and v19.2.0 and above, undici will default to using Node's [File](https://nodejs.org/api/buffer.html#class-file) class. In versions where it's not available, it will default to the undici one. | ||
## FormData | ||
@@ -15,0 +9,0 @@ |
@@ -9,2 +9,3 @@ 'use strict' | ||
const ProxyAgent = require('./lib/dispatcher/proxy-agent') | ||
const EnvHttpProxyAgent = require('./lib/dispatcher/env-http-proxy-agent') | ||
const RetryAgent = require('./lib/dispatcher/retry-agent') | ||
@@ -34,2 +35,3 @@ const errors = require('./lib/core/errors') | ||
module.exports.ProxyAgent = ProxyAgent | ||
module.exports.EnvHttpProxyAgent = EnvHttpProxyAgent | ||
module.exports.RetryAgent = RetryAgent | ||
@@ -121,3 +123,3 @@ module.exports.RetryHandler = RetryHandler | ||
module.exports.FormData = require('./lib/web/fetch/formdata').FormData | ||
module.exports.File = require('./lib/web/fetch/file').File | ||
module.exports.File = globalThis.File ?? require('node:buffer').File | ||
module.exports.FileReader = require('./lib/web/fileapi/filereader').FileReader | ||
@@ -124,0 +126,0 @@ |
@@ -15,9 +15,16 @@ const assert = require('node:assert') | ||
for await (const chunk of body) { | ||
chunks.push(chunk) | ||
length += chunk.length | ||
if (length > CHUNK_LIMIT) { | ||
chunks = null | ||
break | ||
try { | ||
for await (const chunk of body) { | ||
chunks.push(chunk) | ||
length += chunk.length | ||
if (length > CHUNK_LIMIT) { | ||
chunks = [] | ||
length = 0 | ||
break | ||
} | ||
} | ||
} catch { | ||
chunks = [] | ||
length = 0 | ||
// Do nothing.... | ||
} | ||
@@ -27,3 +34,3 @@ | ||
if (statusCode === 204 || !contentType || !chunks) { | ||
if (statusCode === 204 || !contentType || !length) { | ||
queueMicrotask(() => callback(new ResponseStatusCodeError(message, statusCode, headers))) | ||
@@ -30,0 +37,0 @@ return |
@@ -10,3 +10,3 @@ 'use strict' | ||
isValidHTTPToken, | ||
isValidHeaderChar, | ||
isValidHeaderValue, | ||
isStream, | ||
@@ -340,3 +340,3 @@ destroy, | ||
if (typeof val[i] === 'string') { | ||
if (!isValidHeaderChar(val[i])) { | ||
if (!isValidHeaderValue(val[i])) { | ||
throw new InvalidArgumentError(`invalid ${key} header`) | ||
@@ -355,3 +355,3 @@ } | ||
} else if (typeof val === 'string') { | ||
if (!isValidHeaderChar(val)) { | ||
if (!isValidHeaderValue(val)) { | ||
throw new InvalidArgumentError(`invalid ${key} header`) | ||
@@ -361,4 +361,2 @@ } | ||
val = '' | ||
} else if (typeof val === 'object') { | ||
throw new InvalidArgumentError(`invalid ${key} header`) | ||
} else { | ||
@@ -365,0 +363,0 @@ val = `${val}` |
@@ -63,3 +63,6 @@ module.exports = { | ||
kHTTPContext: Symbol('http context'), | ||
kMaxConcurrentStreams: Symbol('max concurrent streams') | ||
kMaxConcurrentStreams: Symbol('max concurrent streams'), | ||
kNoProxyAgent: Symbol('no proxy agent'), | ||
kHttpProxyAgent: Symbol('http proxy agent'), | ||
kHttpsProxyAgent: Symbol('https proxy agent') | ||
} |
@@ -55,2 +55,28 @@ 'use strict' | ||
function isValidPort (port) { | ||
const value = parseInt(port, 10) | ||
return ( | ||
value === Number(port) && | ||
value >= 0 && | ||
value <= 65535 | ||
) | ||
} | ||
function isHttpOrHttpsPrefixed (value) { | ||
return ( | ||
value != null && | ||
value[0] === 'h' && | ||
value[1] === 't' && | ||
value[2] === 't' && | ||
value[3] === 'p' && | ||
( | ||
value[4] === ':' || | ||
( | ||
value[4] === 's' && | ||
value[5] === ':' | ||
) | ||
) | ||
) | ||
} | ||
function parseURL (url) { | ||
@@ -60,3 +86,3 @@ if (typeof url === 'string') { | ||
if (!/^https?:/.test(url.origin || url.protocol)) { | ||
if (!isHttpOrHttpsPrefixed(url.origin || url.protocol)) { | ||
throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.') | ||
@@ -72,8 +98,4 @@ } | ||
if (!/^https?:/.test(url.origin || url.protocol)) { | ||
throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.') | ||
} | ||
if (!(url instanceof URL)) { | ||
if (url.port != null && url.port !== '' && !Number.isFinite(parseInt(url.port))) { | ||
if (url.port != null && url.port !== '' && isValidPort(url.port) === false) { | ||
throw new InvalidArgumentError('Invalid URL: port must be a valid integer or a string representation of an integer.') | ||
@@ -98,2 +120,6 @@ } | ||
if (!isHttpOrHttpsPrefixed(url.origin || url.protocol)) { | ||
throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.') | ||
} | ||
const port = url.port != null | ||
@@ -104,3 +130,3 @@ ? url.port | ||
? url.origin | ||
: `${url.protocol}//${url.hostname}:${port}` | ||
: `${url.protocol || ''}//${url.hostname || ''}:${port}` | ||
let path = url.path != null | ||
@@ -110,7 +136,7 @@ ? url.path | ||
if (origin.endsWith('/')) { | ||
origin = origin.substring(0, origin.length - 1) | ||
if (origin[origin.length - 1] === '/') { | ||
origin = origin.slice(0, origin.length - 1) | ||
} | ||
if (path && !path.startsWith('/')) { | ||
if (path && path[0] !== '/') { | ||
path = `/${path}` | ||
@@ -122,5 +148,9 @@ } | ||
// If first parameter is an absolute URL, a given second param will be ignored. | ||
url = new URL(origin + path) | ||
return new URL(`${origin}${path}`) | ||
} | ||
if (!isHttpOrHttpsPrefixed(url.origin || url.protocol)) { | ||
throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.') | ||
} | ||
return url | ||
@@ -203,7 +233,2 @@ } | ||
function isReadableAborted (stream) { | ||
const state = stream?._readableState | ||
return isDestroyed(stream) && state && !state.endEmitted | ||
} | ||
function destroy (stream, err) { | ||
@@ -533,3 +558,3 @@ if (stream == null || !isStream(stream) || isDestroyed(stream)) { | ||
*/ | ||
function isValidHeaderChar (characters) { | ||
function isValidHeaderValue (characters) { | ||
return !headerCharRegex.test(characters) | ||
@@ -587,3 +612,2 @@ } | ||
isUSVString, | ||
isReadableAborted, | ||
isBlobLike, | ||
@@ -616,9 +640,10 @@ parseOrigin, | ||
isValidHTTPToken, | ||
isValidHeaderChar, | ||
isValidHeaderValue, | ||
isTokenCharCode, | ||
parseRangeHeader, | ||
isValidPort, | ||
isHttpOrHttpsPrefixed, | ||
nodeMajor, | ||
nodeMinor, | ||
nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13), | ||
safeHTTPMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE'] | ||
} |
@@ -211,6 +211,5 @@ 'use strict' | ||
* along with the socket right away | ||
* Find a way to trigger the close cycle from here on. | ||
*/ | ||
function onHTTP2GoAway (code) { | ||
const err = new InformationalError(`HTTP/2: "GOAWAY" frame received with code ${code}`) | ||
const err = new RequestAbortedError(`HTTP/2: "GOAWAY" frame received with code ${code}`) | ||
@@ -224,4 +223,3 @@ // We need to trigger the close cycle right away | ||
this.unref() | ||
// We send the GOAWAY frame response as no error | ||
this.destroy() | ||
util.destroy(this[kSocket], err) | ||
@@ -228,0 +226,0 @@ } |
@@ -206,3 +206,3 @@ // @ts-check | ||
timeout: connectTimeout, | ||
...(util.nodeHasAutoSelectFamily && autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), | ||
...(autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), | ||
...connect | ||
@@ -380,2 +380,3 @@ }) | ||
const requests = client[kQueue].splice(client[kRunningIdx]) | ||
for (let i = 0; i < requests.length; i++) { | ||
@@ -382,0 +383,0 @@ const request = requests[i] |
@@ -9,6 +9,4 @@ 'use strict' | ||
} = require('../core/errors') | ||
const { kDestroy, kClose, kDispatch, kInterceptors } = require('../core/symbols') | ||
const { kDestroy, kClose, kClosed, kDestroyed, kDispatch, kInterceptors } = require('../core/symbols') | ||
const kDestroyed = Symbol('destroyed') | ||
const kClosed = Symbol('closed') | ||
const kOnDestroyed = Symbol('onDestroyed') | ||
@@ -15,0 +13,0 @@ const kOnClosed = Symbol('onClosed') |
@@ -61,3 +61,3 @@ 'use strict' | ||
timeout: connectTimeout, | ||
...(util.nodeHasAutoSelectFamily && autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), | ||
...(autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), | ||
...connect | ||
@@ -64,0 +64,0 @@ }) |
@@ -505,4 +505,3 @@ 'use strict' | ||
new AbortController().signal, | ||
'immutable', | ||
{ settingsObject: request.client } | ||
'immutable' | ||
) | ||
@@ -783,3 +782,3 @@ // 5.4.2.1 | ||
// 5.5.2.1 | ||
const responseObject = fromInnerResponse(response, 'immutable', { settingsObject: {} }) | ||
const responseObject = fromInnerResponse(response, 'immutable') | ||
@@ -786,0 +785,0 @@ responseList.push(responseObject.clone()) |
@@ -6,3 +6,2 @@ 'use strict' | ||
const { makeRequest } = require('../fetch/request') | ||
const { getGlobalOrigin } = require('../fetch/global') | ||
const { webidl } = require('../fetch/webidl') | ||
@@ -15,2 +14,3 @@ const { EventSourceStream } = require('./eventsource-stream') | ||
const { kEnumerableProperty } = require('../../core/util') | ||
const { environmentSettingsObject } = require('../fetch/util') | ||
@@ -71,8 +71,2 @@ let experimentalWarned = false | ||
/** | ||
* @typedef {object} EventSourceInit | ||
* @property {boolean} [withCredentials] indicates whether the request | ||
* should include credentials. | ||
*/ | ||
/** | ||
* The EventSource interface is used to receive server-sent events. It | ||
@@ -100,9 +94,8 @@ * connects to a server over HTTP and receives events in text/event-stream | ||
#dispatcher | ||
/** | ||
* @type {object} | ||
* @property {string} lastEventId | ||
* @property {number} reconnectionTime | ||
* @property {any} reconnectionTimer | ||
* @type {import('./eventsource-stream').eventSourceSettings} | ||
*/ | ||
#settings = null | ||
#state | ||
@@ -131,9 +124,4 @@ /** | ||
// 2. Let settings be ev's relevant settings object. | ||
// https://html.spec.whatwg.org/multipage/webappapis.html#environment-settings-object | ||
this.#settings = { | ||
origin: getGlobalOrigin(), | ||
policyContainer: { | ||
referrerPolicy: 'no-referrer' | ||
}, | ||
this.#dispatcher = eventSourceInitDict.dispatcher | ||
this.#state = { | ||
lastEventId: '', | ||
@@ -143,2 +131,6 @@ reconnectionTime: defaultReconnectionTime | ||
// 2. Let settings be ev's relevant settings object. | ||
// https://html.spec.whatwg.org/multipage/webappapis.html#environment-settings-object | ||
const settings = environmentSettingsObject | ||
let urlRecord | ||
@@ -148,4 +140,4 @@ | ||
// 3. Let urlRecord be the result of encoding-parsing a URL given url, relative to settings. | ||
urlRecord = new URL(url, this.#settings.origin) | ||
this.#settings.origin = urlRecord.origin | ||
urlRecord = new URL(url, settings.settingsObject.baseUrl) | ||
this.#state.origin = urlRecord.origin | ||
} catch (e) { | ||
@@ -184,3 +176,3 @@ // 4. If urlRecord is failure, then throw a "SyntaxError" DOMException. | ||
// 9. Set request's client to settings. | ||
initRequest.client = this.#settings | ||
initRequest.client = environmentSettingsObject.settingsObject | ||
@@ -236,4 +228,5 @@ // 10. User agents may set (`Accept`, `text/event-stream`) in request's header list. | ||
const fetchParam = { | ||
request: this.#request | ||
const fetchParams = { | ||
request: this.#request, | ||
dispatcher: this.#dispatcher | ||
} | ||
@@ -252,6 +245,6 @@ | ||
// 15. Fetch request, with processResponseEndOfBody set to processEventSourceEndOfBody... | ||
fetchParam.processResponseEndOfBody = processEventSourceEndOfBody | ||
fetchParams.processResponseEndOfBody = processEventSourceEndOfBody | ||
// and processResponse set to the following steps given response res: | ||
fetchParam.processResponse = (response) => { | ||
fetchParams.processResponse = (response) => { | ||
// 1. If res is an aborted network error, then fail the connection. | ||
@@ -305,6 +298,6 @@ | ||
// If redirected to a different origin, set the origin to the new origin. | ||
this.#settings.origin = response.urlList[response.urlList.length - 1].origin | ||
this.#state.origin = response.urlList[response.urlList.length - 1].origin | ||
const eventSourceStream = new EventSourceStream({ | ||
eventSourceSettings: this.#settings, | ||
eventSourceSettings: this.#state, | ||
push: (event) => { | ||
@@ -330,3 +323,3 @@ this.dispatchEvent(new MessageEvent( | ||
this.#controller = fetching(fetchParam) | ||
this.#controller = fetching(fetchParams) | ||
} | ||
@@ -356,3 +349,3 @@ | ||
// 2. Wait a delay equal to the reconnection time of the event source. | ||
await delay(this.#settings.reconnectionTime) | ||
await delay(this.#state.reconnectionTime) | ||
@@ -372,4 +365,4 @@ // 5. Queue a task to run the following steps: | ||
// list. | ||
if (this.#settings.lastEventId !== '') { | ||
this.#request.headersList.set('last-event-id', this.#settings.lastEventId, true) | ||
if (this.#state.lastEventId.length) { | ||
this.#request.headersList.set('last-event-id', this.#state.lastEventId, true) | ||
} | ||
@@ -390,8 +383,4 @@ | ||
this.#readyState = CLOSED | ||
clearTimeout(this.#settings.reconnectionTimer) | ||
this.#controller.abort() | ||
if (this.#request) { | ||
this.#request = null | ||
} | ||
this.#request = null | ||
} | ||
@@ -489,3 +478,11 @@ | ||
webidl.converters.EventSourceInitDict = webidl.dictionaryConverter([ | ||
{ key: 'withCredentials', converter: webidl.converters.boolean, defaultValue: false } | ||
{ | ||
key: 'withCredentials', | ||
converter: webidl.converters.boolean, | ||
defaultValue: false | ||
}, | ||
{ | ||
key: 'dispatcher', // undici only | ||
converter: webidl.converters.any | ||
} | ||
]) | ||
@@ -492,0 +489,0 @@ |
@@ -312,3 +312,3 @@ 'use strict' | ||
return new Blob([bytes], { type: mimeType }) | ||
}, instance) | ||
}, instance, false) | ||
}, | ||
@@ -322,4 +322,5 @@ | ||
return consumeBody(this, (bytes) => { | ||
return new Uint8Array(bytes).buffer | ||
}, instance) | ||
// Note: arrayBuffer already cloned. | ||
return bytes.buffer | ||
}, instance, true) | ||
}, | ||
@@ -330,3 +331,3 @@ | ||
// consume body with this and UTF-8 decode. | ||
return consumeBody(this, utf8DecodeBytes, instance) | ||
return consumeBody(this, utf8DecodeBytes, instance, false) | ||
}, | ||
@@ -337,3 +338,3 @@ | ||
// consume body with this and parse JSON from bytes. | ||
return consumeBody(this, parseJSONFromBytes, instance) | ||
return consumeBody(this, parseJSONFromBytes, instance, false) | ||
}, | ||
@@ -390,3 +391,3 @@ | ||
) | ||
}, instance) | ||
}, instance, false) | ||
} | ||
@@ -407,4 +408,5 @@ } | ||
* @param {Response|Request} instance | ||
* @param {boolean} [shouldClone] | ||
*/ | ||
async function consumeBody (object, convertBytesToJSValue, instance) { | ||
async function consumeBody (object, convertBytesToJSValue, instance, shouldClone) { | ||
webidl.brandCheck(object, instance) | ||
@@ -415,3 +417,3 @@ | ||
if (bodyUnusable(object[kState].body)) { | ||
throw new TypeError('Body is unusable') | ||
throw new TypeError('Body is unusable: Body has already been read') | ||
} | ||
@@ -442,3 +444,3 @@ | ||
if (object[kState].body == null) { | ||
successSteps(new Uint8Array()) | ||
successSteps(Buffer.allocUnsafe(0)) | ||
return promise.promise | ||
@@ -449,3 +451,3 @@ } | ||
// errorSteps, and object’s relevant global object. | ||
await fullyReadBody(object[kState].body, successSteps, errorSteps) | ||
await fullyReadBody(object[kState].body, successSteps, errorSteps, shouldClone) | ||
@@ -452,0 +454,0 @@ // 7. Return promise. |
'use strict' | ||
const { EOL } = require('node:os') | ||
const { Blob, File: NativeFile } = require('node:buffer') | ||
const { types } = require('node:util') | ||
const { Blob, File } = require('node:buffer') | ||
const { kState } = require('./symbols') | ||
const { isBlobLike } = require('./util') | ||
const { webidl } = require('./webidl') | ||
const { parseMIMEType, serializeAMimeType } = require('./data-url') | ||
const { kEnumerableProperty } = require('../../core/util') | ||
const encoder = new TextEncoder() | ||
class File extends Blob { | ||
constructor (fileBits, fileName, options = {}) { | ||
// The File constructor is invoked with two or three parameters, depending | ||
// on whether the optional dictionary parameter is used. When the File() | ||
// constructor is invoked, user agents must run the following steps: | ||
webidl.argumentLengthCheck(arguments, 2, { header: 'File constructor' }) | ||
fileBits = webidl.converters['sequence<BlobPart>'](fileBits) | ||
fileName = webidl.converters.USVString(fileName) | ||
options = webidl.converters.FilePropertyBag(options) | ||
// 1. Let bytes be the result of processing blob parts given fileBits and | ||
// options. | ||
// Note: Blob handles this for us | ||
// 2. Let n be the fileName argument to the constructor. | ||
const n = fileName | ||
// 3. Process FilePropertyBag dictionary argument by running the following | ||
// substeps: | ||
// 1. If the type member is provided and is not the empty string, let t | ||
// be set to the type dictionary member. If t contains any characters | ||
// outside the range U+0020 to U+007E, then set t to the empty string | ||
// and return from these substeps. | ||
// 2. Convert every character in t to ASCII lowercase. | ||
let t = options.type | ||
let d | ||
// eslint-disable-next-line no-labels | ||
substep: { | ||
if (t) { | ||
t = parseMIMEType(t) | ||
if (t === 'failure') { | ||
t = '' | ||
// eslint-disable-next-line no-labels | ||
break substep | ||
} | ||
t = serializeAMimeType(t).toLowerCase() | ||
} | ||
// 3. If the lastModified member is provided, let d be set to the | ||
// lastModified dictionary member. If it is not provided, set d to the | ||
// current date and time represented as the number of milliseconds since | ||
// the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]). | ||
d = options.lastModified | ||
} | ||
// 4. Return a new File object F such that: | ||
// F refers to the bytes byte sequence. | ||
// F.size is set to the number of total bytes in bytes. | ||
// F.name is set to n. | ||
// F.type is set to t. | ||
// F.lastModified is set to d. | ||
super(processBlobParts(fileBits, options), { type: t }) | ||
this[kState] = { | ||
name: n, | ||
lastModified: d, | ||
type: t | ||
} | ||
} | ||
get name () { | ||
webidl.brandCheck(this, File) | ||
return this[kState].name | ||
} | ||
get lastModified () { | ||
webidl.brandCheck(this, File) | ||
return this[kState].lastModified | ||
} | ||
get type () { | ||
webidl.brandCheck(this, File) | ||
return this[kState].type | ||
} | ||
} | ||
// TODO(@KhafraDev): remove | ||
class FileLike { | ||
@@ -199,125 +109,4 @@ constructor (blobLike, fileName, options = {}) { | ||
Object.defineProperties(File.prototype, { | ||
[Symbol.toStringTag]: { | ||
value: 'File', | ||
configurable: true | ||
}, | ||
name: kEnumerableProperty, | ||
lastModified: kEnumerableProperty | ||
}) | ||
webidl.converters.Blob = webidl.interfaceConverter(Blob) | ||
webidl.converters.BlobPart = function (V, opts) { | ||
if (webidl.util.Type(V) === 'Object') { | ||
if (isBlobLike(V)) { | ||
return webidl.converters.Blob(V, { strict: false }) | ||
} | ||
if (ArrayBuffer.isView(V) || types.isAnyArrayBuffer(V)) { | ||
return webidl.converters.BufferSource(V, opts) | ||
} | ||
} | ||
return webidl.converters.USVString(V, opts) | ||
} | ||
webidl.converters['sequence<BlobPart>'] = webidl.sequenceConverter( | ||
webidl.converters.BlobPart | ||
) | ||
// https://www.w3.org/TR/FileAPI/#dfn-FilePropertyBag | ||
webidl.converters.FilePropertyBag = webidl.dictionaryConverter([ | ||
{ | ||
key: 'lastModified', | ||
converter: webidl.converters['long long'], | ||
get defaultValue () { | ||
return Date.now() | ||
} | ||
}, | ||
{ | ||
key: 'type', | ||
converter: webidl.converters.DOMString, | ||
defaultValue: '' | ||
}, | ||
{ | ||
key: 'endings', | ||
converter: (value) => { | ||
value = webidl.converters.DOMString(value) | ||
value = value.toLowerCase() | ||
if (value !== 'native') { | ||
value = 'transparent' | ||
} | ||
return value | ||
}, | ||
defaultValue: 'transparent' | ||
} | ||
]) | ||
/** | ||
* @see https://www.w3.org/TR/FileAPI/#process-blob-parts | ||
* @param {(NodeJS.TypedArray|Blob|string)[]} parts | ||
* @param {{ type: string, endings: string }} options | ||
*/ | ||
function processBlobParts (parts, options) { | ||
// 1. Let bytes be an empty sequence of bytes. | ||
/** @type {NodeJS.TypedArray[]} */ | ||
const bytes = [] | ||
// 2. For each element in parts: | ||
for (const element of parts) { | ||
// 1. If element is a USVString, run the following substeps: | ||
if (typeof element === 'string') { | ||
// 1. Let s be element. | ||
let s = element | ||
// 2. If the endings member of options is "native", set s | ||
// to the result of converting line endings to native | ||
// of element. | ||
if (options.endings === 'native') { | ||
s = convertLineEndingsNative(s) | ||
} | ||
// 3. Append the result of UTF-8 encoding s to bytes. | ||
bytes.push(encoder.encode(s)) | ||
} else if (ArrayBuffer.isView(element) || types.isArrayBuffer(element)) { | ||
// 2. If element is a BufferSource, get a copy of the | ||
// bytes held by the buffer source, and append those | ||
// bytes to bytes. | ||
if (element.buffer) { | ||
bytes.push( | ||
new Uint8Array(element.buffer, element.byteOffset, element.byteLength) | ||
) | ||
} else { // ArrayBuffer | ||
bytes.push(new Uint8Array(element)) | ||
} | ||
} else if (isBlobLike(element)) { | ||
// 3. If element is a Blob, append the bytes it represents | ||
// to bytes. | ||
bytes.push(element) | ||
} | ||
} | ||
// 3. Return bytes. | ||
return bytes | ||
} | ||
/** | ||
* @see https://www.w3.org/TR/FileAPI/#convert-line-endings-to-native | ||
* @param {string} s | ||
*/ | ||
function convertLineEndingsNative (s) { | ||
// 1. Let native line ending be be the code point U+000A LF. | ||
// 2. If the underlying platform’s conventions are to | ||
// represent newlines as a carriage return and line feed | ||
// sequence, set native line ending to the code point | ||
// U+000D CR followed by the code point U+000A LF. | ||
// NOTE: We are using the native line ending for the current | ||
// platform, provided by node's os module. | ||
return s.replace(/\r?\n/g, EOL) | ||
} | ||
// If this function is moved to ./util.js, some tools (such as | ||
@@ -328,4 +117,4 @@ // rollup) will warn about circular dependencies. See: | ||
return ( | ||
(NativeFile && object instanceof NativeFile) || | ||
object instanceof File || ( | ||
(object instanceof File) || | ||
( | ||
object && | ||
@@ -339,2 +128,2 @@ (typeof object.stream === 'function' || | ||
module.exports = { File, FileLike, isFileLike } | ||
module.exports = { FileLike, isFileLike } |
@@ -6,3 +6,3 @@ 'use strict' | ||
const { HTTP_TOKEN_CODEPOINTS, isomorphicDecode } = require('./data-url') | ||
const { isFileLike, File: UndiciFile } = require('./file') | ||
const { isFileLike } = require('./file') | ||
const { makeEntry } = require('./formdata') | ||
@@ -12,3 +12,3 @@ const assert = require('node:assert') | ||
const File = globalThis.File ?? NodeFile ?? UndiciFile | ||
const File = globalThis.File ?? NodeFile | ||
@@ -15,0 +15,0 @@ const formDataNameBuffer = Buffer.from('form-data; name="') |
@@ -6,3 +6,3 @@ 'use strict' | ||
const { kEnumerableProperty } = require('../../core/util') | ||
const { File: UndiciFile, FileLike, isFileLike } = require('./file') | ||
const { FileLike, isFileLike } = require('./file') | ||
const { webidl } = require('./webidl') | ||
@@ -13,3 +13,3 @@ const { File: NativeFile } = require('node:buffer') | ||
/** @type {globalThis['File']} */ | ||
const File = NativeFile ?? UndiciFile | ||
const File = globalThis.File ?? NativeFile | ||
@@ -236,3 +236,3 @@ // https://xhr.spec.whatwg.org/#formdata | ||
value = (NativeFile && value instanceof NativeFile) || value instanceof UndiciFile | ||
value = value instanceof NativeFile | ||
? new File([value], filename, options) | ||
@@ -239,0 +239,0 @@ : new FileLike(value, filename, options) |
@@ -14,3 +14,3 @@ /* globals AbortController */ | ||
normalizeMethod, | ||
makePolicyContainer, | ||
environmentSettingsObject, | ||
normalizeMethodRecord | ||
@@ -29,5 +29,4 @@ } = require('./util') | ||
const { kEnumerableProperty } = util | ||
const { kHeaders, kSignal, kState, kGuard, kRealm, kDispatcher } = require('./symbols') | ||
const { kHeaders, kSignal, kState, kGuard, kDispatcher } = require('./symbols') | ||
const { webidl } = require('./webidl') | ||
const { getGlobalOrigin } = require('./global') | ||
const { URLSerializer } = require('./data-url') | ||
@@ -59,13 +58,2 @@ const { kHeadersList, kConstruct } = require('../../core/symbols') | ||
// https://html.spec.whatwg.org/multipage/webappapis.html#environment-settings-object | ||
this[kRealm] = { | ||
settingsObject: { | ||
baseUrl: getGlobalOrigin(), | ||
get origin () { | ||
return this.baseUrl?.origin | ||
}, | ||
policyContainer: makePolicyContainer() | ||
} | ||
} | ||
// 1. Let request be null. | ||
@@ -78,3 +66,3 @@ let request = null | ||
// 3. Let baseURL be this’s relevant settings object’s API base URL. | ||
const baseUrl = this[kRealm].settingsObject.baseUrl | ||
const baseUrl = environmentSettingsObject.settingsObject.baseUrl | ||
@@ -126,3 +114,3 @@ // 4. Let signal be null. | ||
// 7. Let origin be this’s relevant settings object’s origin. | ||
const origin = this[kRealm].settingsObject.origin | ||
const origin = environmentSettingsObject.settingsObject.origin | ||
@@ -163,3 +151,3 @@ // 8. Let window be "client". | ||
// client This’s relevant settings object. | ||
client: this[kRealm].settingsObject, | ||
client: environmentSettingsObject.settingsObject, | ||
// window window. | ||
@@ -253,3 +241,3 @@ window, | ||
(parsedReferrer.protocol === 'about:' && parsedReferrer.hostname === 'client') || | ||
(origin && !sameOrigin(parsedReferrer, this[kRealm].settingsObject.baseUrl)) | ||
(origin && !sameOrigin(parsedReferrer, environmentSettingsObject.settingsObject.baseUrl)) | ||
) { | ||
@@ -376,3 +364,2 @@ request.referrer = 'client' | ||
this[kSignal] = ac.signal | ||
this[kSignal][kRealm] = this[kRealm] | ||
@@ -447,3 +434,2 @@ // 29. If signal is not null, then make this’s signal follow signal. | ||
this[kHeaders][kGuard] = 'request' | ||
this[kHeaders][kRealm] = this[kRealm] | ||
@@ -782,3 +768,3 @@ // 31. If this’s request’s mode is "no-cors", then: | ||
// 4. Return clonedRequestObject. | ||
return fromInnerRequest(clonedRequest, ac.signal, this[kHeaders][kGuard], this[kRealm]) | ||
return fromInnerRequest(clonedRequest, ac.signal, this[kHeaders][kGuard]) | ||
} | ||
@@ -886,15 +872,11 @@ | ||
* @param {'request' | 'immutable' | 'request-no-cors' | 'response' | 'none'} guard | ||
* @param {any} [realm] | ||
* @returns {Request} | ||
*/ | ||
function fromInnerRequest (innerRequest, signal, guard, realm) { | ||
function fromInnerRequest (innerRequest, signal, guard) { | ||
const request = new Request(kConstruct) | ||
request[kState] = innerRequest | ||
request[kRealm] = realm | ||
request[kSignal] = signal | ||
request[kSignal][kRealm] = realm | ||
request[kHeaders] = new Headers(kConstruct) | ||
request[kHeaders][kHeadersList] = innerRequest.headersList | ||
request[kHeaders][kGuard] = guard | ||
request[kHeaders][kRealm] = realm | ||
return request | ||
@@ -901,0 +883,0 @@ } |
@@ -15,3 +15,4 @@ 'use strict' | ||
isErrorLike, | ||
isomorphicEncode | ||
isomorphicEncode, | ||
environmentSettingsObject: relevantRealm | ||
} = require('./util') | ||
@@ -22,6 +23,5 @@ const { | ||
} = require('./constants') | ||
const { kState, kHeaders, kGuard, kRealm } = require('./symbols') | ||
const { kState, kHeaders, kGuard } = require('./symbols') | ||
const { webidl } = require('./webidl') | ||
const { FormData } = require('./formdata') | ||
const { getGlobalOrigin } = require('./global') | ||
const { URLSerializer } = require('./data-url') | ||
@@ -38,9 +38,6 @@ const { kHeadersList, kConstruct } = require('../../core/symbols') | ||
static error () { | ||
// TODO | ||
const relevantRealm = { settingsObject: {} } | ||
// The static error() method steps are to return the result of creating a | ||
// Response object, given a new network error, "immutable", and this’s | ||
// relevant Realm. | ||
const responseObject = fromInnerResponse(makeNetworkError(), 'immutable', relevantRealm) | ||
const responseObject = fromInnerResponse(makeNetworkError(), 'immutable') | ||
@@ -68,4 +65,3 @@ return responseObject | ||
// "response", and this’s relevant Realm. | ||
const relevantRealm = { settingsObject: {} } | ||
const responseObject = fromInnerResponse(makeResponse({}), 'response', relevantRealm) | ||
const responseObject = fromInnerResponse(makeResponse({}), 'response') | ||
@@ -81,4 +77,2 @@ // 4. Perform initialize a response given responseObject, init, and (body, "application/json"). | ||
static redirect (url, status = 302) { | ||
const relevantRealm = { settingsObject: {} } | ||
webidl.argumentLengthCheck(arguments, 1, { header: 'Response.redirect' }) | ||
@@ -95,3 +89,3 @@ | ||
try { | ||
parsedURL = new URL(url, getGlobalOrigin()) | ||
parsedURL = new URL(url, relevantRealm.settingsObject.baseUrl) | ||
} catch (err) { | ||
@@ -108,3 +102,3 @@ throw new TypeError(`Failed to parse URL from ${url}`, { cause: err }) | ||
// given a new response, "immutable", and this’s relevant Realm. | ||
const responseObject = fromInnerResponse(makeResponse({}), 'immutable', relevantRealm) | ||
const responseObject = fromInnerResponse(makeResponse({}), 'immutable') | ||
@@ -136,5 +130,2 @@ // 5. Set responseObject’s response’s status to status. | ||
// TODO | ||
this[kRealm] = { settingsObject: {} } | ||
// 1. Set this’s response to a new response. | ||
@@ -149,3 +140,2 @@ this[kState] = makeResponse({}) | ||
this[kHeaders][kHeadersList] = this[kState].headersList | ||
this[kHeaders][kRealm] = this[kRealm] | ||
@@ -263,3 +253,3 @@ // 3. Let bodyWithType be null. | ||
// clonedResponse, this’s headers’s guard, and this’s relevant Realm. | ||
return fromInnerResponse(clonedResponse, this[kHeaders][kGuard], this[kRealm]) | ||
return fromInnerResponse(clonedResponse, this[kHeaders][kGuard]) | ||
} | ||
@@ -525,13 +515,10 @@ | ||
* @param {'request' | 'immutable' | 'request-no-cors' | 'response' | 'none'} guard | ||
* @param {any} [realm] | ||
* @returns {Response} | ||
*/ | ||
function fromInnerResponse (innerResponse, guard, realm) { | ||
function fromInnerResponse (innerResponse, guard) { | ||
const response = new Response(kConstruct) | ||
response[kState] = innerResponse | ||
response[kRealm] = realm | ||
response[kHeaders] = new Headers(kConstruct) | ||
response[kHeaders][kHeadersList] = innerResponse.headersList | ||
response[kHeaders][kGuard] = guard | ||
response[kHeaders][kRealm] = realm | ||
return response | ||
@@ -538,0 +525,0 @@ } |
@@ -9,4 +9,3 @@ 'use strict' | ||
kGuard: Symbol('guard'), | ||
kRealm: Symbol('realm'), | ||
kDispatcher: Symbol('dispatcher') | ||
} |
@@ -1046,3 +1046,3 @@ 'use strict' | ||
*/ | ||
async function fullyReadBody (body, processBody, processBodyError) { | ||
async function fullyReadBody (body, processBody, processBodyError, shouldClone) { | ||
// 1. If taskDestination is null, then set taskDestination to | ||
@@ -1073,4 +1073,3 @@ // the result of starting a new parallel queue. | ||
try { | ||
const result = await readAllBytes(reader) | ||
successSteps(result) | ||
successSteps(await readAllBytes(reader, shouldClone)) | ||
} catch (e) { | ||
@@ -1103,2 +1102,4 @@ errorSteps(e) | ||
const invalidIsomorphicEncodeValueRegex = /[^\x00-\xFF]/ // eslint-disable-line | ||
/** | ||
@@ -1110,5 +1111,3 @@ * @see https://infra.spec.whatwg.org/#isomorphic-encode | ||
// 1. Assert: input contains no code points greater than U+00FF. | ||
for (let i = 0; i < input.length; i++) { | ||
assert(input.charCodeAt(i) <= 0xFF) | ||
} | ||
assert(!invalidIsomorphicEncodeValueRegex.test(input)) | ||
@@ -1125,4 +1124,5 @@ // 2. Return a byte sequence whose length is equal to input’s code | ||
* @param {ReadableStreamDefaultReader} reader | ||
* @param {boolean} [shouldClone] | ||
*/ | ||
async function readAllBytes (reader) { | ||
async function readAllBytes (reader, shouldClone) { | ||
const bytes = [] | ||
@@ -1136,2 +1136,9 @@ let byteLength = 0 | ||
// 1. Call successSteps with bytes. | ||
if (bytes.length === 1) { | ||
const { buffer, byteOffset, byteLength } = bytes[0] | ||
if (shouldClone === false) { | ||
return Buffer.from(buffer, byteOffset, byteLength) | ||
} | ||
return Buffer.from(buffer.slice(byteOffset, byteOffset + byteLength), 0, byteLength) | ||
} | ||
return Buffer.concat(bytes, byteLength) | ||
@@ -1571,2 +1578,20 @@ } | ||
class EnvironmentSettingsObjectBase { | ||
get baseUrl () { | ||
return getGlobalOrigin() | ||
} | ||
get origin () { | ||
return this.baseUrl?.origin | ||
} | ||
policyContainer = makePolicyContainer() | ||
} | ||
class EnvironmentSettingsObject { | ||
settingsObject = new EnvironmentSettingsObjectBase() | ||
} | ||
const environmentSettingsObject = new EnvironmentSettingsObject() | ||
module.exports = { | ||
@@ -1623,3 +1648,4 @@ isAborted, | ||
getDecodeSplit, | ||
utf8DecodeBytes | ||
utf8DecodeBytes, | ||
environmentSettingsObject | ||
} |
@@ -214,15 +214,8 @@ 'use strict' | ||
? fatalDecoder.decode.bind(fatalDecoder) | ||
: !isUtf8 | ||
? function () { // TODO: remove once node 18 or < node v18.14.0 is dropped | ||
process.emitWarning('ICU is not supported and no fallback exists. Please upgrade to at least Node v18.14.0.', { | ||
code: 'UNDICI-WS-NO-ICU' | ||
}) | ||
throw new TypeError('Invalid utf-8 received.') | ||
} | ||
: function (buffer) { | ||
if (isUtf8(buffer)) { | ||
return buffer.toString('utf-8') | ||
} | ||
throw new TypeError('Invalid utf-8 received.') | ||
} | ||
: function (buffer) { | ||
if (isUtf8(buffer)) { | ||
return buffer.toString('utf-8') | ||
} | ||
throw new TypeError('Invalid utf-8 received.') | ||
} | ||
@@ -229,0 +222,0 @@ module.exports = { |
{ | ||
"name": "undici", | ||
"version": "6.13.0", | ||
"version": "6.14.0", | ||
"description": "An HTTP/1.1 client, written from scratch for Node.js", | ||
@@ -107,3 +107,3 @@ "homepage": "https://undici.nodejs.org", | ||
"abort-controller": "^3.0.0", | ||
"borp": "^0.10.0", | ||
"borp": "^0.11.0", | ||
"c8": "^9.1.0", | ||
@@ -122,2 +122,3 @@ "cross-env": "^7.0.3", | ||
"proxy": "^2.1.1", | ||
"sinon": "^17.0.1", | ||
"snazzy": "^9.0.0", | ||
@@ -130,3 +131,3 @@ "standard": "^17.0.0", | ||
"engines": { | ||
"node": ">=18.0" | ||
"node": ">=18.17" | ||
}, | ||
@@ -133,0 +134,0 @@ "standard": { |
@@ -220,3 +220,3 @@ import { URL } from 'url' | ||
/** Invoked before request is dispatched on socket. May be invoked multiple times when a request is retried when the request at the head of the pipeline fails. */ | ||
onConnect?(abort: () => void): void; | ||
onConnect?(abort: (err?: Error) => void): void; | ||
/** Invoked when an error has occurred. */ | ||
@@ -223,0 +223,0 @@ onError?(err: Error): void; |
import { MessageEvent, ErrorEvent } from './websocket' | ||
import Dispatcher from './dispatcher' | ||
@@ -53,3 +54,3 @@ import { | ||
prototype: EventSource | ||
new (url: string | URL, init: EventSourceInit): EventSource | ||
new (url: string | URL, init?: EventSourceInit): EventSource | ||
readonly CLOSED: 2 | ||
@@ -61,3 +62,4 @@ readonly CONNECTING: 0 | ||
interface EventSourceInit { | ||
withCredentials?: boolean | ||
withCredentials?: boolean, | ||
dispatcher?: Dispatcher | ||
} |
@@ -17,2 +17,3 @@ import Dispatcher from'./dispatcher' | ||
import ProxyAgent from'./proxy-agent' | ||
import EnvHttpProxyAgent from './env-http-proxy-agent' | ||
import RetryHandler from'./retry-handler' | ||
@@ -35,3 +36,3 @@ import RetryAgent from'./retry-agent' | ||
export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent, RedirectHandler, DecoratorHandler, RetryHandler, RetryAgent } | ||
export { Dispatcher, BalancedPool, Pool, Client, buildConnector, errors, Agent, request, stream, pipeline, connect, upgrade, setGlobalDispatcher, getGlobalDispatcher, setGlobalOrigin, getGlobalOrigin, MockClient, MockPool, MockAgent, mockErrors, ProxyAgent, EnvHttpProxyAgent, RedirectHandler, DecoratorHandler, RetryHandler, RetryAgent } | ||
export default Undici | ||
@@ -38,0 +39,0 @@ |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 6 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
1106809
24
171
23272
13