undici
Advanced tools
Comparing version 6.12.0 to 6.13.0
@@ -66,5 +66,3 @@ // Ported from https://github.com/nodejs/undici/pull/907 | ||
// never get a chance and will always encounter an unhandled exception. | ||
// - tick => process.nextTick(fn) | ||
// - micro tick => queueMicrotask(fn) | ||
queueMicrotask(() => { | ||
setImmediate(() => { | ||
callback(err) | ||
@@ -71,0 +69,0 @@ }) |
@@ -101,24 +101,18 @@ 'use strict' | ||
const { [kClient]: client } = this | ||
const { [kSocket]: socket } = client | ||
const err = this[kSocket][kError] || new SocketError('closed', util.getSocketInfo(this)) | ||
const err = this[kSocket][kError] || this[kError] || new SocketError('closed', util.getSocketInfo(socket)) | ||
client[kSocket] = null | ||
client[kHTTP2Session] = null | ||
assert(client[kPending] === 0) | ||
if (client.destroyed) { | ||
assert(client[kPending] === 0) | ||
// Fail entire queue. | ||
const requests = client[kQueue].splice(client[kRunningIdx]) | ||
for (let i = 0; i < requests.length; i++) { | ||
const request = requests[i] | ||
util.errorRequest(client, request, err) | ||
// Fail entire queue. | ||
const requests = client[kQueue].splice(client[kRunningIdx]) | ||
for (let i = 0; i < requests.length; i++) { | ||
const request = requests[i] | ||
util.errorRequest(client, request, err) | ||
} | ||
} | ||
client[kPendingIdx] = client[kRunningIdx] | ||
assert(client[kRunning] === 0) | ||
client.emit('disconnect', client[kUrl], [client], err) | ||
client[kResume]() | ||
}) | ||
@@ -143,2 +137,20 @@ | ||
util.addListener(socket, 'close', function () { | ||
const err = this[kError] || new SocketError('closed', util.getSocketInfo(this)) | ||
client[kSocket] = null | ||
if (this[kHTTP2Session] != null) { | ||
this[kHTTP2Session].destroy(err) | ||
} | ||
client[kPendingIdx] = client[kRunningIdx] | ||
assert(client[kRunning] === 0) | ||
client.emit('disconnect', client[kUrl], [client], err) | ||
client[kResume]() | ||
}) | ||
let closed = false | ||
@@ -160,6 +172,6 @@ socket.on('close', () => { | ||
destroy (err, callback) { | ||
session.destroy(err) | ||
if (closed) { | ||
queueMicrotask(callback) | ||
} else { | ||
// Destroying the socket will trigger the session close | ||
socket.destroy(err).on('close', callback) | ||
@@ -263,23 +275,24 @@ } | ||
try { | ||
// We are already connected, streams are pending. | ||
// We can call on connect, and wait for abort | ||
request.onConnect((err) => { | ||
if (request.aborted || request.completed) { | ||
return | ||
} | ||
const abort = (err) => { | ||
if (request.aborted || request.completed) { | ||
return | ||
} | ||
err = err || new RequestAbortedError() | ||
err = err || new RequestAbortedError() | ||
if (stream != null) { | ||
util.destroy(stream, err) | ||
util.errorRequest(client, request, err) | ||
session[kOpenStreams] -= 1 | ||
if (session[kOpenStreams] === 0) { | ||
session.unref() | ||
} | ||
} | ||
if (stream != null) { | ||
util.destroy(stream, err) | ||
} | ||
util.errorRequest(client, request, err) | ||
}) | ||
// We do not destroy the socket as we can continue using the session | ||
// the stream get's destroyed and the session remains to create new streams | ||
util.destroy(body, err) | ||
} | ||
try { | ||
// We are already connected, streams are pending. | ||
// We can call on connect, and wait for abort | ||
request.onConnect(abort) | ||
} catch (err) { | ||
@@ -309,3 +322,2 @@ util.errorRequest(client, request, err) | ||
session[kOpenStreams] -= 1 | ||
// TODO(HTTP/2): unref only if current streams count is 0 | ||
if (session[kOpenStreams] === 0) session.unref() | ||
@@ -390,3 +402,3 @@ }) | ||
// Increment counter as we have new several streams open | ||
// Increment counter as we have new streams open | ||
++session[kOpenStreams] | ||
@@ -403,3 +415,3 @@ | ||
// as there's no value to keep it open. | ||
if (request.aborted || request.completed) { | ||
if (request.aborted) { | ||
const err = new RequestAbortedError() | ||
@@ -434,3 +446,2 @@ util.errorRequest(client, request, err) | ||
// have yet RST_STREAM support on client-side | ||
session[kOpenStreams] -= 1 | ||
if (session[kOpenStreams] === 0) { | ||
@@ -440,5 +451,3 @@ session.unref() | ||
const err = new InformationalError('HTTP/2: stream half-closed (remote)') | ||
util.errorRequest(client, request, err) | ||
util.destroy(stream, err) | ||
abort(new InformationalError('HTTP/2: stream half-closed (remote)')) | ||
}) | ||
@@ -454,17 +463,7 @@ | ||
stream.once('error', function (err) { | ||
if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) { | ||
session[kOpenStreams] -= 1 | ||
util.errorRequest(client, request, err) | ||
util.destroy(stream, err) | ||
} | ||
abort(err) | ||
}) | ||
stream.once('frameError', (type, code) => { | ||
const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`) | ||
util.errorRequest(client, request, err) | ||
if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) { | ||
session[kOpenStreams] -= 1 | ||
util.destroy(stream, err) | ||
} | ||
abort(new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`)) | ||
}) | ||
@@ -492,26 +491,39 @@ | ||
/* istanbul ignore else: assertion */ | ||
if (!body) { | ||
request.onRequestSent() | ||
if (!body || contentLength === 0) { | ||
writeBuffer({ | ||
abort, | ||
client, | ||
request, | ||
contentLength, | ||
expectsPayload, | ||
h2stream: stream, | ||
body: null, | ||
socket: client[kSocket] | ||
}) | ||
} else if (util.isBuffer(body)) { | ||
assert(contentLength === body.byteLength, 'buffer body must have content length') | ||
stream.cork() | ||
stream.write(body) | ||
stream.uncork() | ||
stream.end() | ||
request.onBodySent(body) | ||
request.onRequestSent() | ||
writeBuffer({ | ||
abort, | ||
client, | ||
request, | ||
contentLength, | ||
body, | ||
expectsPayload, | ||
h2stream: stream, | ||
socket: client[kSocket] | ||
}) | ||
} else if (util.isBlobLike(body)) { | ||
if (typeof body.stream === 'function') { | ||
writeIterable({ | ||
abort, | ||
client, | ||
request, | ||
contentLength, | ||
expectsPayload, | ||
h2stream: stream, | ||
expectsPayload, | ||
body: body.stream(), | ||
socket: client[kSocket], | ||
header: '' | ||
socket: client[kSocket] | ||
}) | ||
} else { | ||
writeBlob({ | ||
abort, | ||
body, | ||
@@ -523,3 +535,2 @@ client, | ||
h2stream: stream, | ||
header: '', | ||
socket: client[kSocket] | ||
@@ -556,3 +567,26 @@ }) | ||
function writeStream ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { | ||
function writeBuffer ({ abort, h2stream, body, client, request, socket, contentLength, expectsPayload }) { | ||
try { | ||
if (body != null && util.isBuffer(body)) { | ||
assert(contentLength === body.byteLength, 'buffer body must have content length') | ||
h2stream.cork() | ||
h2stream.write(body) | ||
h2stream.uncork() | ||
h2stream.end() | ||
request.onBodySent(body) | ||
} | ||
if (!expectsPayload) { | ||
socket[kReset] = true | ||
} | ||
request.onRequestSent() | ||
client[kResume]() | ||
} catch (error) { | ||
abort(error) | ||
} | ||
} | ||
function writeStream ({ abort, socket, expectsPayload, h2stream, body, client, request, contentLength }) { | ||
assert(contentLength !== 0 || client[kRunning] === 0, 'stream body cannot be pipelined') | ||
@@ -566,6 +600,13 @@ | ||
if (err) { | ||
util.destroy(body, err) | ||
util.destroy(h2stream, err) | ||
util.destroy(pipe, err) | ||
abort(err) | ||
} else { | ||
util.removeAllListeners(pipe) | ||
request.onRequestSent() | ||
if (!expectsPayload) { | ||
socket[kReset] = true | ||
} | ||
client[kResume]() | ||
} | ||
@@ -575,7 +616,3 @@ } | ||
pipe.on('data', onPipeData) | ||
pipe.once('end', () => { | ||
pipe.removeListener('data', onPipeData) | ||
util.destroy(pipe) | ||
}) | ||
util.addListener(pipe, 'data', onPipeData) | ||
@@ -587,3 +624,3 @@ function onPipeData (chunk) { | ||
async function writeBlob ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { | ||
async function writeBlob ({ abort, h2stream, body, client, request, socket, contentLength, expectsPayload }) { | ||
assert(contentLength === body.size, 'blob body must have content length') | ||
@@ -601,2 +638,3 @@ | ||
h2stream.uncork() | ||
h2stream.end() | ||
@@ -612,7 +650,7 @@ request.onBodySent(buffer) | ||
} catch (err) { | ||
util.destroy(h2stream) | ||
abort(err) | ||
} | ||
} | ||
async function writeIterable ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { | ||
async function writeIterable ({ abort, h2stream, body, client, request, socket, contentLength, expectsPayload }) { | ||
assert(contentLength !== 0 || client[kRunning] === 0, 'iterator body cannot be pipelined') | ||
@@ -656,7 +694,15 @@ | ||
} | ||
h2stream.end() | ||
request.onRequestSent() | ||
if (!expectsPayload) { | ||
socket[kReset] = true | ||
} | ||
client[kResume]() | ||
} catch (err) { | ||
h2stream.destroy(err) | ||
abort(err) | ||
} finally { | ||
request.onRequestSent() | ||
h2stream.end() | ||
h2stream | ||
@@ -663,0 +709,0 @@ .off('close', onDrain) |
'use strict' | ||
module.exports = class DecoratorHandler { | ||
#handler | ||
constructor (handler) { | ||
this.handler = handler | ||
if (typeof handler !== 'object' || handler === null) { | ||
throw new TypeError('handler must be an object') | ||
} | ||
this.#handler = handler | ||
} | ||
onConnect (...args) { | ||
return this.handler.onConnect(...args) | ||
return this.#handler.onConnect?.(...args) | ||
} | ||
onError (...args) { | ||
return this.handler.onError(...args) | ||
return this.#handler.onError?.(...args) | ||
} | ||
onUpgrade (...args) { | ||
return this.handler.onUpgrade(...args) | ||
return this.#handler.onUpgrade?.(...args) | ||
} | ||
onResponseStarted (...args) { | ||
return this.#handler.onResponseStarted?.(...args) | ||
} | ||
onHeaders (...args) { | ||
return this.handler.onHeaders(...args) | ||
return this.#handler.onHeaders?.(...args) | ||
} | ||
onData (...args) { | ||
return this.handler.onData(...args) | ||
return this.#handler.onData?.(...args) | ||
} | ||
onComplete (...args) { | ||
return this.handler.onComplete(...args) | ||
return this.#handler.onComplete?.(...args) | ||
} | ||
onBodySent (...args) { | ||
return this.handler.onBodySent(...args) | ||
return this.#handler.onBodySent?.(...args) | ||
} | ||
} |
@@ -406,4 +406,2 @@ 'use strict' | ||
throwIfAborted(object[kState]) | ||
// 1. If object is unusable, then return a promise rejected | ||
@@ -415,2 +413,4 @@ // with a TypeError. | ||
throwIfAborted(object[kState]) | ||
// 2. Let promise be a new promise. | ||
@@ -417,0 +417,0 @@ const promise = createDeferredPromise() |
'use strict' | ||
const { toUSVString, isUSVString, bufferToLowerCasedHeaderName } = require('../../core/util') | ||
const { isUSVString, bufferToLowerCasedHeaderName } = require('../../core/util') | ||
const { utf8DecodeBytes } = require('./util') | ||
@@ -64,39 +64,2 @@ const { HTTP_TOKEN_CODEPOINTS, isomorphicDecode } = require('./data-url') | ||
/** | ||
* @see https://andreubotella.github.io/multipart-form-data/#escape-a-multipart-form-data-name | ||
* @param {string} name | ||
* @param {string} [encoding='utf-8'] | ||
* @param {boolean} [isFilename=false] | ||
*/ | ||
function escapeFormDataName (name, encoding = 'utf-8', isFilename = false) { | ||
// 1. If isFilename is true: | ||
if (isFilename) { | ||
// 1.1. Set name to the result of converting name into a scalar value string. | ||
name = toUSVString(name) | ||
} else { | ||
// 2. Otherwise: | ||
// 2.1. Assert: name is a scalar value string. | ||
assert(isUSVString(name)) | ||
// 2.2. Replace every occurrence of U+000D (CR) not followed by U+000A (LF), | ||
// and every occurrence of U+000A (LF) not preceded by U+000D (CR), in | ||
// name, by a string consisting of U+000D (CR) and U+000A (LF). | ||
name = name.replace(/\r\n?|\r?\n/g, '\r\n') | ||
} | ||
// 3. Let encoded be the result of encoding name with encoding. | ||
assert(Buffer.isEncoding(encoding)) | ||
// 4. Replace every 0x0A (LF) bytes in encoded with the byte sequence `%0A`, | ||
// 0x0D (CR) with `%0D` and 0x22 (") with `%22`. | ||
name = name | ||
.replace(/\n/g, '%0A') | ||
.replace(/\r/g, '%0D') | ||
.replace(/"/g, '%22') | ||
// 5. Return encoded. | ||
return Buffer.from(name, encoding) // encoded | ||
} | ||
/** | ||
* @see https://andreubotella.github.io/multipart-form-data/#multipart-form-data-parser | ||
@@ -501,4 +464,3 @@ * @param {Buffer} input | ||
multipartFormDataParser, | ||
validateBoundary, | ||
escapeFormDataName | ||
validateBoundary | ||
} |
@@ -76,12 +76,11 @@ 'use strict' | ||
function isValidEncodedURL (url) { | ||
for (const c of url) { | ||
const code = c.charCodeAt(0) | ||
// Not used in US-ASCII | ||
if (code >= 0x80) { | ||
for (let i = 0; i < url.length; ++i) { | ||
const code = url.charCodeAt(i) | ||
if ( | ||
code > 0x7E || // Non-US-ASCII + DEL | ||
code < 0x20 // Control characters NUL - US | ||
) { | ||
return false | ||
} | ||
// Control characters | ||
if ((code >= 0x00 && code <= 0x1F) || code === 0x7F) { | ||
return false | ||
} | ||
} | ||
@@ -164,20 +163,11 @@ return true | ||
// - Contains no 0x00 (NUL) or HTTP newline bytes. | ||
if ( | ||
potentialValue.startsWith('\t') || | ||
potentialValue.startsWith(' ') || | ||
potentialValue.endsWith('\t') || | ||
potentialValue.endsWith(' ') | ||
) { | ||
return false | ||
} | ||
if ( | ||
potentialValue.includes('\0') || | ||
return ( | ||
potentialValue[0] === '\t' || | ||
potentialValue[0] === ' ' || | ||
potentialValue[potentialValue.length - 1] === '\t' || | ||
potentialValue[potentialValue.length - 1] === ' ' || | ||
potentialValue.includes('\n') || | ||
potentialValue.includes('\r') || | ||
potentialValue.includes('\n') | ||
) { | ||
return false | ||
} | ||
return true | ||
potentialValue.includes('\0') | ||
) === false | ||
} | ||
@@ -1174,9 +1164,17 @@ | ||
* @param {string|URL} url | ||
* @returns {boolean} | ||
*/ | ||
function urlHasHttpsScheme (url) { | ||
if (typeof url === 'string') { | ||
return url.startsWith('https:') | ||
} | ||
return url.protocol === 'https:' | ||
return ( | ||
( | ||
typeof url === 'string' && | ||
url[5] === ':' && | ||
url[0] === 'h' && | ||
url[1] === 't' && | ||
url[2] === 't' && | ||
url[3] === 'p' && | ||
url[4] === 's' | ||
) || | ||
url.protocol === 'https:' | ||
) | ||
} | ||
@@ -1573,2 +1571,3 @@ | ||
isCancelled, | ||
isValidEncodedURL, | ||
createDeferredPromise, | ||
@@ -1575,0 +1574,0 @@ ReadableStreamFrom, |
@@ -212,21 +212,18 @@ 'use strict' | ||
*/ | ||
function utf8Decode (buffer) { | ||
if (hasIntl) { | ||
return fatalDecoder.decode(buffer) | ||
} else { | ||
if (!isUtf8?.(buffer)) { | ||
// TODO: remove once node 18 or < node v18.14.0 is dropped | ||
if (!isUtf8) { | ||
const utf8Decode = hasIntl | ||
? 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.') | ||
} | ||
throw new TypeError('Invalid utf-8 received.') | ||
} | ||
return buffer.toString('utf-8') | ||
} | ||
} | ||
module.exports = { | ||
@@ -233,0 +230,0 @@ isConnecting, |
{ | ||
"name": "undici", | ||
"version": "6.12.0", | ||
"version": "6.13.0", | ||
"description": "An HTTP/1.1 client, written from scratch for Node.js", | ||
@@ -5,0 +5,0 @@ "homepage": "https://undici.nodejs.org", |
@@ -251,2 +251,14 @@ # undici | ||
[FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) besides text data and buffers can also utilize streams via [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) objects: | ||
```js | ||
import { openAsBlob } from 'node:fs' | ||
const file = await openAsBlob('./big.csv') | ||
const body = new FormData() | ||
body.set('file', file, 'big.csv') | ||
await fetch('http://example.com', { method: 'POST', body }) | ||
``` | ||
#### `request.duplex` | ||
@@ -253,0 +265,0 @@ |
Sorry, the diff of this file is too big to display
467
1132835
23559