Comparing version 4.11.3 to 4.12.0
@@ -153,3 +153,3 @@ 'use strict' | ||
function isAborted (stream) { | ||
function isReadableAborted (stream) { | ||
const state = stream && stream._readableState | ||
@@ -248,11 +248,24 @@ return isDestroyed(stream) && state && !state.endEmitted | ||
function isDisturbed (body) { | ||
const state = body && body._readableState | ||
return !!(body && ( | ||
(stream.isDisturbed && stream.isDisturbed(body)) || | ||
body[kBodyUsed] || | ||
body.readableDidRead || (state && state.dataEmitted) || | ||
isAborted(body) | ||
stream.isDisturbed | ||
? stream.isDisturbed(body) || body[kBodyUsed] // TODO (fix): Why is body[kBodyUsed] needed? | ||
: body[kBodyUsed] || | ||
body.readableDidRead || | ||
(body._readableState && body._readableState.dataEmitted) || | ||
isReadableAborted(body) | ||
)) | ||
} | ||
function isErrored (body) { | ||
return !!(body && ( | ||
stream.isErrored | ||
? stream.isErrored(body) | ||
: /state: 'errored'/.test(nodeUtil.inspect(body) | ||
))) | ||
} | ||
function isReadable (body) { | ||
return !!(body && /state: 'readable'/.test(nodeUtil.inspect(body))) | ||
} | ||
function getSocketInfo (socket) { | ||
@@ -315,4 +328,6 @@ return { | ||
isDisturbed, | ||
isErrored, | ||
isReadable, | ||
toUSVString: nodeUtil.toUSVString || ((val) => `${val}`), | ||
isAborted, | ||
isReadableAborted, | ||
isBlobLike, | ||
@@ -319,0 +334,0 @@ parseOrigin, |
@@ -6,3 +6,3 @@ 'use strict' | ||
const { FormData } = require('./formdata') | ||
const { kState, kError } = require('./symbols') | ||
const { kState } = require('./symbols') | ||
const { Blob } = require('buffer') | ||
@@ -13,2 +13,3 @@ const { kBodyUsed } = require('../core/symbols') | ||
const { NotSupportedError } = require('../core/errors') | ||
const { isErrored } = require('../core/util') | ||
@@ -192,3 +193,3 @@ let ReadableStream | ||
// bytes into stream. | ||
if (!/state: 'errored'/.test(nodeUtil.inspect(stream))) { | ||
if (!isErrored(stream)) { | ||
controller.enqueue(new Uint8Array(value)) | ||
@@ -274,6 +275,2 @@ } | ||
if (stream[kError]) { | ||
throw stream[kError] | ||
} | ||
if (util.isDisturbed(stream)) { | ||
@@ -359,8 +356,2 @@ throw new TypeError('disturbed') | ||
function cancelBody (body, reason) { | ||
if (body.stream && !/state: 'errored'/.test(nodeUtil.inspect(body.stream))) { | ||
body.stream.cancel(reason) | ||
} | ||
} | ||
function mixinBody (prototype) { | ||
@@ -372,3 +363,2 @@ Object.assign(prototype, methods) | ||
module.exports = { | ||
cancelBody, | ||
extractBody, | ||
@@ -375,0 +365,0 @@ safelyExtractBody, |
@@ -27,3 +27,3 @@ // https://github.com/Ethan-Arrowood/undici-fetch | ||
tryUpgradeRequestToAPotentiallyTrustworthyURL, | ||
makeTimingInfo, | ||
createOpaqueTimingInfo, | ||
appendFetchMetadata, | ||
@@ -35,6 +35,6 @@ corsCheck, | ||
} = require('./util') | ||
const { kState, kHeaders, kGuard, kRealm, kError } = require('./symbols') | ||
const { kState, kHeaders, kGuard, kRealm } = require('./symbols') | ||
const { AbortError } = require('../core/errors') | ||
const assert = require('assert') | ||
const { safelyExtractBody, cancelBody } = require('./body') | ||
const { safelyExtractBody } = require('./body') | ||
const { | ||
@@ -49,11 +49,29 @@ redirectStatus, | ||
const EE = require('events') | ||
const { PassThrough, pipeline, compose } = require('stream') | ||
const { PassThrough, pipeline } = require('stream') | ||
const { isErrored, isReadable } = require('../core/util') | ||
let ReadableStream | ||
// https://fetch.spec.whatwg.org/#garbage-collection | ||
const registry = new FinalizationRegistry((abort) => { | ||
abort() | ||
}) | ||
class Fetch extends EE { | ||
constructor (dispatcher) { | ||
super() | ||
this.dispatcher = dispatcher | ||
this.terminated = null | ||
this.connection = null | ||
this.dump = false | ||
} | ||
terminate ({ reason, aborted } = {}) { | ||
if (this.terminated) { | ||
return | ||
} | ||
this.terminated = { aborted, reason } | ||
this.connection?.destroy(reason) | ||
this.emit('terminated', reason) | ||
} | ||
} | ||
// https://fetch.spec.whatwg.org/#fetch-method | ||
@@ -79,22 +97,4 @@ async function fetch (...args) { | ||
const context = Object.assign(new EE(), { | ||
dispatcher: this, | ||
terminated: false, | ||
connection: null, | ||
dump: false, | ||
terminate ({ reason, aborted } = {}) { | ||
if (this.terminated) { | ||
return | ||
} | ||
this.terminated = { aborted } | ||
const context = new Fetch(this) | ||
if (this.connection) { | ||
this.connection.destroy(reason) | ||
this.connection = null | ||
} | ||
this.emit('terminated', reason) | ||
} | ||
}) | ||
// 1. Let p be a new promise. | ||
@@ -160,3 +160,3 @@ const p = createDeferredPromise() | ||
// 12. Fetch request with processResponseDone set to handleFetchDone, | ||
// 12. Fetch request with processResponseEndOfBody set to handleFetchDone, | ||
// and processResponse given response being these substeps: | ||
@@ -201,3 +201,3 @@ const processResponse = (response) => { | ||
request, | ||
processResponseDone: handleFetchDone, | ||
processResponseEndOfBody: handleFetchDone, | ||
processResponse | ||
@@ -235,7 +235,5 @@ }) | ||
if (!timingInfo.timingAllowPassed) { | ||
// 1. Set timingInfo to a new fetch timing info whose start time and | ||
// post-redirect start time are timingInfo’s start time. | ||
timingInfo = makeTimingInfo({ | ||
startTime: timingInfo.startTime, | ||
postRedirectStartTime: timingInfo.postRedirectStartTime | ||
// 1. Set timingInfo to a the result of creating an opaque timing info for timingInfo. | ||
timingInfo = createOpaqueTimingInfo({ | ||
startTime: timingInfo.startTime | ||
}) | ||
@@ -283,4 +281,10 @@ | ||
// body with error. | ||
if (request.body != null) { | ||
cancelBody(request.body, error) | ||
if (request.body != null && isReadable(request.body?.stream)) { | ||
request.body.stream.cancel(error).catch((err) => { | ||
if (err.code === 'ERR_INVALID_STATE') { | ||
// Node bug? | ||
return | ||
} | ||
throw err | ||
}) | ||
} | ||
@@ -298,4 +302,10 @@ | ||
// body with error. | ||
if (response.body != null) { | ||
cancelBody(response.body, error) | ||
if (response.body != null && isReadable(response.body?.stream)) { | ||
response.body.stream.cancel(error).catch((err) => { | ||
if (err.code === 'ERR_INVALID_STATE') { | ||
// Node bug? | ||
return | ||
} | ||
throw err | ||
}) | ||
} | ||
@@ -305,3 +315,11 @@ } | ||
// https://fetch.spec.whatwg.org/#fetching | ||
function fetching ({ request, processResponse, processResponseDone }) { | ||
function fetching ({ | ||
request, | ||
processRequestBodyChunkLength, | ||
processRequestEndOfBody, | ||
processResponse, | ||
processResponseEndOfBody, | ||
processResponseConsumeBody, | ||
useParallelQueue = false, | ||
}) { | ||
// 1. Let taskDestination be null. | ||
@@ -332,22 +350,24 @@ let taskDestination = null | ||
const currenTime = coarsenedSharedCurrentTime(crossOriginIsolatedCapability) | ||
const timingInfo = makeTimingInfo({ | ||
startTime: currenTime, | ||
postRedirectStartTime: currenTime | ||
const timingInfo = createOpaqueTimingInfo({ | ||
startTime: currenTime | ||
}) | ||
// 6. Let fetchParams be a new fetch params whose request is request, timing | ||
// info is timingInfo, process request body is processRequestBody, | ||
// process request end-of-body is processRequestEndOfBody, process response | ||
// is processResponse, process response end-of-body is | ||
// processResponseEndOfBody, process response done is processResponseDone, | ||
// task destination is taskDestination, and cross-origin isolated capability | ||
// is crossOriginIsolatedCapability. | ||
// 6. Let fetchParams be a new fetch params whose | ||
// request is request, | ||
// timing info is timingInfo, | ||
// process request body chunk length is processRequestBodyChunkLength, | ||
// process request end-of-body is processRequestEndOfBody, | ||
// process response is processResponse, | ||
// process response consume body is processResponseConsumeBody, | ||
// process response end-of-body is processResponseEndOfBody, | ||
// task destination is taskDestination, | ||
// and cross-origin isolated capability is crossOriginIsolatedCapability. | ||
const fetchParams = { | ||
request, | ||
timingInfo, | ||
processRequestBody: null, | ||
processRequestEndOfBody: null, | ||
processRequestBodyChunkLength, | ||
processRequestEndOfBody, | ||
processResponse, | ||
processResponseEndOfBody: null, | ||
processResponseDone, | ||
processResponseConsumeBody, | ||
processResponseEndOfBody, | ||
taskDestination, | ||
@@ -670,6 +690,5 @@ crossOriginIsolatedCapability | ||
// 2. If request’s response tainting is "opaque", response is a network | ||
// error, or response’s body is null, then run processBodyError and abort | ||
// these steps. | ||
if (request.responseTainting === 'opaque' && response.status === 0) { | ||
// 2. If request’s response tainting is "opaque", or response’s body is null, | ||
// then run processBodyError and abort these steps. | ||
if (request.responseTainting === 'opaque' || response.body == null) { | ||
processBodyError(response.error) | ||
@@ -732,9 +751,9 @@ return | ||
// 2. If fetchParams’s process response end-of-body is non-null, then:. | ||
// 2. If fetchParams’s process response consume is non-null, then:. | ||
// TODO | ||
// 1. Let processBody given nullOrBytes be this step: run fetchParams’s | ||
// process response end-of-body given response and nullOrBytes.on. | ||
// process response consume given response and nullOrBytes.on. | ||
// TODO | ||
// 2. Let processBodyError be this step: run fetchParams’s process | ||
// response end-of-body given response and failure.on. | ||
// response consume given response and failure.on. | ||
// TODO | ||
@@ -1272,7 +1291,11 @@ // 3. If response’s body is null, then queue a fetch task to run | ||
// 1. Let aborted be the termination’s aborted flag. | ||
const aborted = context.terminated.aborted | ||
// 2. If aborted is set, then return an aborted network error. | ||
if (aborted) { | ||
return makeNetworkError(new AbortError()) | ||
} | ||
// 3. Return a network error. | ||
return makeNetworkError( | ||
context.terminated.aborted ? new AbortError() : null | ||
) | ||
return makeNetworkError(context.terminated.reason) | ||
} | ||
@@ -1307,6 +1330,8 @@ | ||
// 2. If aborted is set, then return an aborted network error. | ||
const reason = aborted ? new AbortError() : new Error('terminated') | ||
if (aborted) { | ||
return makeNetworkError(new AbortError()) | ||
} | ||
// 3. Return a network error. | ||
return makeNetworkError(reason) | ||
return makeNetworkError(context.terminated.reason) | ||
} | ||
@@ -1498,9 +1523,11 @@ | ||
// 2. If connection uses HTTP/2, then transmit an RST_STREAM frame. | ||
this.connection?.destroy() | ||
this.connection.destroy() | ||
// 3. If aborted is set, then return an aborted network error. | ||
const reason = aborted ? new AbortError() : new Error('terminated') | ||
if (aborted) { | ||
return resolve(makeNetworkError(new AbortError())) | ||
} | ||
// 4. Return a network error. | ||
resolve(makeNetworkError(reason)) | ||
return resolve(makeNetworkError(this.terminated.reason)) | ||
} | ||
@@ -1550,3 +1577,2 @@ | ||
async cancel (reason) { | ||
stream[kError] = reason | ||
await cancelAlgorithm(reason) | ||
@@ -1590,19 +1616,9 @@ } | ||
// 2. If stream is readable, error stream with an "AbortError" DOMException. | ||
try { | ||
if (isReadable(stream)) { | ||
this.controller.error(new AbortError()) | ||
} catch (err) { | ||
// Will throw TypeError if body is not readable. | ||
if (err.name !== 'TypeError') { | ||
throw err | ||
} | ||
} | ||
} else { | ||
// 4. Otherwise, if stream is readable, error stream with a TypeError. | ||
try { | ||
if (isReadable(stream)) { | ||
this.controller.error(new TypeError('terminated')) | ||
} catch (err) { | ||
// Will throw TypeError if body is not readable. | ||
if (err.name !== 'TypeError') { | ||
throw err | ||
} | ||
} | ||
@@ -1613,3 +1629,3 @@ } | ||
// 6. Otherwise, the user agent should close connection unless it would be bad for performance to do so. | ||
this.connection?.destroy() | ||
this.connection.destroy() | ||
} | ||
@@ -1660,4 +1676,2 @@ | ||
registry.register(stream, this.abort, this) | ||
response = makeResponse({ | ||
@@ -1695,26 +1709,13 @@ status, | ||
let iterator | ||
if (decoders.length > 1) { | ||
if (compose) { | ||
this.decoder = compose(...decoders) | ||
iterator = this.decoder[Symbol.asyncIterator]() | ||
} else { | ||
this.decoder = new PassThrough() | ||
iterator = pipeline(this.decoder, ...decoders, () => {})[ | ||
Symbol.asyncIterator | ||
]() | ||
} | ||
} else if (decoders.length === 1) { | ||
this.decoder = decoders[0] | ||
iterator = this.decoder[Symbol.asyncIterator]() | ||
} else { | ||
this.decoder = new PassThrough() | ||
iterator = this.decoder[Symbol.asyncIterator]() | ||
pipeline(...decoders, () => {}) | ||
} else if (decoders.length === 0) { | ||
// TODO (perf): Avoid intermediate. | ||
decoders.push(new PassThrough()) | ||
} | ||
if (this.decoder) { | ||
this.decoder.on('drain', resume) | ||
} | ||
this.decoder = decoders[0].on('drain', resume) | ||
const iterator = decoders[decoders.length - 1][Symbol.asyncIterator]() | ||
pullAlgorithm = async (controller) => { | ||
@@ -1762,4 +1763,4 @@ // 4. Set bytes to the result of handling content codings given | ||
// 8. If stream is errored, then terminate the ongoing fetch. | ||
if (stream[kError]) { | ||
this.context.terminate({ reason: stream[kError] }) | ||
if (isErrored(stream)) { | ||
this.context.terminate() | ||
return | ||
@@ -1807,4 +1808,2 @@ } | ||
onComplete () { | ||
registry.unregister(this) | ||
this.decoder.end() | ||
@@ -1814,4 +1813,2 @@ }, | ||
onError (error) { | ||
registry.unregister(this) | ||
this.decoder?.destroy(error) | ||
@@ -1818,0 +1815,0 @@ |
@@ -31,2 +31,6 @@ /* globals AbortController */ | ||
const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => { | ||
signal.removeEventListener('abort', abort) | ||
}) | ||
// https://fetch.spec.whatwg.org/#request-class | ||
@@ -138,4 +142,44 @@ class Request { | ||
request = makeRequest({ | ||
...request, | ||
window | ||
// URL request’s URL. | ||
// undici implementation note: this is set as the first item in request's urlList in makeRequest | ||
// method request’s method. | ||
method: request.method, | ||
// header list A copy of request’s header list. | ||
// undici implementation note: headersList is cloned in makeRequest | ||
headersList: request.headersList, | ||
// unsafe-request flag Set. | ||
unsafeRequest: request.unsafeRequest, | ||
// client This’s relevant settings object. | ||
client: request.client, | ||
// window window. | ||
window, | ||
// priority request’s priority. | ||
priority: request.priority, | ||
// origin request’s origin. The propagation of the origin is only significant for navigation requests | ||
// being handled by a service worker. In this scenario a request can have an origin that is different | ||
// from the current client. | ||
origin: request.origin, | ||
// referrer request’s referrer. | ||
referrer: request.referrer, | ||
// referrer policy request’s referrer policy. | ||
referrerPolicy: request.referrerPolicy, | ||
// mode request’s mode. | ||
mode: request.mode, | ||
// credentials mode request’s credentials mode. | ||
credentials: request.credentials, | ||
// cache mode request’s cache mode. | ||
cache: request.cache, | ||
// redirect mode request’s redirect mode. | ||
redirect: request.redirect, | ||
// integrity metadata request’s integrity metadata. | ||
integrity: request.integrity, | ||
// keepalive request’s keepalive. | ||
keepalive: request.keepalive, | ||
// reload-navigation flag request’s reload-navigation flag. | ||
reloadNavigation: request.reloadNavigation, | ||
// history-navigation flag request’s history-navigation flag. | ||
historyNavigation: request.historyNavigation, | ||
// URL list A clone of request’s URL list. | ||
// undici implementation note: urlList is cloned in makeRequest | ||
urlList: request.urlList | ||
}) | ||
@@ -156,7 +200,16 @@ | ||
// 4. Set request’s referrer to "client" | ||
// 4. Set request’s origin to "client". | ||
request.origin = 'client' | ||
// 5. Set request’s referrer to "client" | ||
request.referrer = 'client' | ||
// 5. Set request’s referrer policy to the empty string. | ||
// 6. Set request’s referrer policy to the empty string. | ||
request.referrerPolicy = '' | ||
// 7. Set request’s URL to request’s current URL. | ||
request.url = request.urlList[request.urlList.length - 1] | ||
// 8. Set request’s URL list to « request’s URL ». | ||
request.urlList = [request.url] | ||
} | ||
@@ -170,3 +223,3 @@ | ||
// 2. If referrer is the empty string, then set request’s referrer to "no-referrer". | ||
if (!referrer === '') { | ||
if (referrer === '') { | ||
request.referrer = 'no-referrer' | ||
@@ -335,10 +388,5 @@ } else { | ||
} else { | ||
// TODO: Remove this listener on failure/success. | ||
signal.addEventListener( | ||
'abort', | ||
function () { | ||
ac.abort() | ||
}, | ||
{ once: true } | ||
) | ||
const abort = () => ac.abort() | ||
signal.addEventListener('abort', abort, { once: true }) | ||
requestFinalizer.register(this, { signal, abort }) | ||
} | ||
@@ -345,0 +393,0 @@ } |
@@ -9,4 +9,3 @@ 'use strict' | ||
kGuard: Symbol('guard'), | ||
kRealm: Symbol('realm'), | ||
kError: Symbol('error') | ||
kRealm: Symbol('realm') | ||
} |
@@ -256,8 +256,9 @@ 'use strict' | ||
function makeTimingInfo (init) { | ||
// https://fetch.spec.whatwg.org/#create-an-opaque-timing-info | ||
function createOpaqueTimingInfo (timingInfo) { | ||
return { | ||
startTime: 0, | ||
startTime: timingInfo.startTime ?? 0, | ||
redirectStartTime: 0, | ||
redirectEndTime: 0, | ||
postRedirectStartTime: 0, | ||
postRedirectStartTime: timingInfo.startTime ?? 0, | ||
finalServiceWorkerStartTime: 0, | ||
@@ -269,4 +270,3 @@ finalNetworkResponseStartTime: 0, | ||
decodedBodySize: 0, | ||
finalConnectionTimingInfo: null, | ||
...init | ||
finalConnectionTimingInfo: null | ||
} | ||
@@ -323,3 +323,3 @@ } | ||
crossOriginResourcePolicyCheck, | ||
makeTimingInfo, | ||
createOpaqueTimingInfo, | ||
setRequestReferrerPolicyOnRedirect, | ||
@@ -326,0 +326,0 @@ isValidHTTPToken, |
{ | ||
"name": "undici", | ||
"version": "4.11.3", | ||
"version": "4.12.0", | ||
"description": "An HTTP/1.1 client, written from scratch for Node.js", | ||
@@ -5,0 +5,0 @@ "homepage": "https://undici.nodejs.org", |
@@ -206,10 +206,10 @@ # undici | ||
The [Fetch Standard](https://fetch.spec.whatwg.org) allows users to skip consuming the response body by relying on | ||
[garbage collection](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management#garbage_collection) to release connection resources. Undici does the same. However, | ||
garbage collection in Node is less aggressive and deterministic (due to the lack | ||
of clear idle periods that browser have through the rendering refresh rate) | ||
which means that leaving the release of connection resources to the garbage collector | ||
can lead to excessive connection usage, reduced performance (due to less connection re-use), | ||
and even stalls or deadlocks when running out of connections. Therefore, it is highly | ||
recommended to always either consume or cancel the response body. | ||
[garbage collection](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management#garbage_collection) to release connection resources. Undici does not do the same. Therefore, it is important to always either consume or cancel the response body. | ||
Garbage collection in Node is less aggressive and deterministic | ||
(due to the lack of clear idle periods that browser have through the rendering refresh rate) | ||
which means that leaving the release of connection resources to the garbage collector can lead | ||
to excessive connection usage, reduced performance (due to less connection re-use), and even | ||
stalls or deadlocks when running out of connections. | ||
```js | ||
@@ -293,3 +293,3 @@ // Do | ||
Undici will immediately pipeline when retrying requests afters a failed | ||
Undici will immediately pipeline when retrying requests after a failed | ||
connection. However, Undici will not retry the first remaining requests in | ||
@@ -296,0 +296,0 @@ the prior pipeline and instead error the corresponding callback/promise/stream. |
642384
10209