Comparing version 6.11.1 to 6.12.0
@@ -972,2 +972,8 @@ # Dispatcher | ||
Emitted when the dispatcher has been disconnected from the origin. | ||
> **Note**: For HTTP/2, this event is also emitted when the dispatcher has received the [GOAWAY Frame](https://webconcepts.info/concepts/http2-frame-type/0x7) with an Error with the message `HTTP/2: "GOAWAY" frame received` and the code `UND_ERR_INFO`. | ||
> Due to nature of the protocol of using binary frames, it is possible that requests gets hanging as a frame can be received between the `HEADER` and `DATA` frames. | ||
> It is recommended to handle this event and close the dispatcher to create a new HTTP/2 session. | ||
### Event: `'connectionError'` | ||
@@ -974,0 +980,0 @@ |
@@ -11,7 +11,10 @@ const { addAbortListener } = require('../core/util') | ||
} else { | ||
self.onError(self[kSignal]?.reason ?? new RequestAbortedError()) | ||
self.reason = self[kSignal]?.reason ?? new RequestAbortedError() | ||
} | ||
removeSignal(self) | ||
} | ||
function addSignal (self, signal) { | ||
self.reason = null | ||
self[kSignal] = null | ||
@@ -18,0 +21,0 @@ self[kListener] = null |
'use strict' | ||
const assert = require('node:assert') | ||
const { AsyncResource } = require('node:async_hooks') | ||
const { InvalidArgumentError, RequestAbortedError, SocketError } = require('../core/errors') | ||
const { InvalidArgumentError, SocketError } = require('../core/errors') | ||
const util = require('../core/util') | ||
@@ -35,6 +36,9 @@ const { addSignal, removeSignal } = require('./abort-signal') | ||
onConnect (abort, context) { | ||
if (!this.callback) { | ||
throw new RequestAbortedError() | ||
if (this.reason) { | ||
abort(this.reason) | ||
return | ||
} | ||
assert(this.callback) | ||
this.abort = abort | ||
@@ -41,0 +45,0 @@ this.context = context |
@@ -150,8 +150,10 @@ 'use strict' | ||
if (this.reason) { | ||
abort(this.reason) | ||
return | ||
} | ||
assert(!res, 'pipeline cannot be retried') | ||
assert(!ret.destroyed) | ||
if (ret.destroyed) { | ||
throw new RequestAbortedError() | ||
} | ||
this.abort = abort | ||
@@ -158,0 +160,0 @@ this.context = context |
'use strict' | ||
const assert = require('node:assert') | ||
const { Readable } = require('./readable') | ||
const { | ||
InvalidArgumentError, | ||
RequestAbortedError | ||
} = require('../core/errors') | ||
const { InvalidArgumentError } = require('../core/errors') | ||
const util = require('../core/util') | ||
@@ -72,6 +70,9 @@ const { getResolveErrorBodyCallback } = require('./util') | ||
onConnect (abort, context) { | ||
if (!this.callback) { | ||
throw new RequestAbortedError() | ||
if (this.reason) { | ||
abort(this.reason) | ||
return | ||
} | ||
assert(this.callback) | ||
this.abort = abort | ||
@@ -78,0 +79,0 @@ this.context = context |
'use strict' | ||
const assert = require('node:assert') | ||
const { finished, PassThrough } = require('node:stream') | ||
const { | ||
InvalidArgumentError, | ||
InvalidReturnValueError, | ||
RequestAbortedError | ||
} = require('../core/errors') | ||
const { InvalidArgumentError, InvalidReturnValueError } = require('../core/errors') | ||
const util = require('../core/util') | ||
@@ -73,6 +70,9 @@ const { getResolveErrorBodyCallback } = require('./util') | ||
onConnect (abort, context) { | ||
if (!this.callback) { | ||
throw new RequestAbortedError() | ||
if (this.reason) { | ||
abort(this.reason) | ||
return | ||
} | ||
assert(this.callback) | ||
this.abort = abort | ||
@@ -79,0 +79,0 @@ this.context = context |
'use strict' | ||
const { InvalidArgumentError, RequestAbortedError, SocketError } = require('../core/errors') | ||
const { InvalidArgumentError, SocketError } = require('../core/errors') | ||
const { AsyncResource } = require('node:async_hooks') | ||
@@ -37,6 +37,9 @@ const util = require('../core/util') | ||
onConnect (abort, context) { | ||
if (!this.callback) { | ||
throw new RequestAbortedError() | ||
if (this.reason) { | ||
abort(this.reason) | ||
return | ||
} | ||
assert(this.callback) | ||
this.abort = abort | ||
@@ -43,0 +46,0 @@ this.context = null |
'use strict' | ||
const assert = require('node:assert') | ||
const { kDestroyed, kBodyUsed } = require('./symbols') | ||
const { kDestroyed, kBodyUsed, kListeners } = require('./symbols') | ||
const { IncomingMessage } = require('node:http') | ||
@@ -25,9 +25,16 @@ const stream = require('node:stream') | ||
function isBlobLike (object) { | ||
return (Blob && object instanceof Blob) || ( | ||
object && | ||
typeof object === 'object' && | ||
(typeof object.stream === 'function' || | ||
typeof object.arrayBuffer === 'function') && | ||
/^(Blob|File)$/.test(object[Symbol.toStringTag]) | ||
) | ||
if (object === null) { | ||
return false | ||
} else if (object instanceof Blob) { | ||
return true | ||
} else if (typeof object !== 'object') { | ||
return false | ||
} else { | ||
const sTag = object[Symbol.toStringTag] | ||
return (sTag === 'Blob' || sTag === 'File') && ( | ||
('stream' in object && typeof object.stream === 'function') || | ||
('arrayBuffer' in object && typeof object.arrayBuffer === 'function') | ||
) | ||
} | ||
} | ||
@@ -538,2 +545,25 @@ | ||
function addListener (obj, name, listener) { | ||
const listeners = (obj[kListeners] ??= []) | ||
listeners.push([name, listener]) | ||
obj.on(name, listener) | ||
return obj | ||
} | ||
function removeAllListeners (obj) { | ||
for (const [name, listener] of obj[kListeners] ?? []) { | ||
obj.removeListener(name, listener) | ||
} | ||
obj[kListeners] = null | ||
} | ||
function errorRequest (client, request, err) { | ||
try { | ||
request.onError(err) | ||
assert(request.aborted) | ||
} catch (err) { | ||
client.emit('error', err) | ||
} | ||
} | ||
const kEnumerableProperty = Object.create(null) | ||
@@ -561,2 +591,5 @@ kEnumerableProperty.enumerable = true | ||
bufferToLowerCasedHeaderName, | ||
addListener, | ||
removeAllListeners, | ||
errorRequest, | ||
parseRawHeaders, | ||
@@ -563,0 +596,0 @@ parseHeaders, |
@@ -50,3 +50,2 @@ 'use strict' | ||
kMaxResponseSize, | ||
kListeners, | ||
kOnError, | ||
@@ -60,19 +59,7 @@ kResume, | ||
const FastBuffer = Buffer[Symbol.species] | ||
const addListener = util.addListener | ||
const removeAllListeners = util.removeAllListeners | ||
let extractBody | ||
function addListener (obj, name, listener) { | ||
const listeners = (obj[kListeners] ??= []) | ||
listeners.push([name, listener]) | ||
obj.on(name, listener) | ||
return obj | ||
} | ||
function removeAllListeners (obj) { | ||
for (const [name, listener] of obj[kListeners] ?? []) { | ||
obj.removeListener(name, listener) | ||
} | ||
obj[kListeners] = null | ||
} | ||
async function lazyllhttp () { | ||
@@ -724,3 +711,3 @@ const llhttpWasmData = process.env.JEST_WORKER_ID ? require('../llhttp/llhttp-wasm.js') : undefined | ||
const request = requests[i] | ||
errorRequest(client, request, err) | ||
util.errorRequest(client, request, err) | ||
} | ||
@@ -732,3 +719,3 @@ } else if (client[kRunning] > 0 && err.code !== 'UND_ERR_INFO') { | ||
errorRequest(client, request, err) | ||
util.errorRequest(client, request, err) | ||
} | ||
@@ -838,11 +825,2 @@ | ||
function errorRequest (client, request, err) { | ||
try { | ||
request.onError(err) | ||
assert(request.aborted) | ||
} catch (err) { | ||
client.emit('error', err) | ||
} | ||
} | ||
// https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2 | ||
@@ -914,3 +892,3 @@ function shouldSendContentLength (method) { | ||
if (client[kStrictContentLength]) { | ||
errorRequest(client, request, new RequestContentLengthMismatchError()) | ||
util.errorRequest(client, request, new RequestContentLengthMismatchError()) | ||
return false | ||
@@ -924,18 +902,20 @@ } | ||
try { | ||
request.onConnect((err) => { | ||
if (request.aborted || request.completed) { | ||
return | ||
} | ||
const abort = (err) => { | ||
if (request.aborted || request.completed) { | ||
return | ||
} | ||
errorRequest(client, request, err || new RequestAbortedError()) | ||
util.errorRequest(client, request, err || new RequestAbortedError()) | ||
util.destroy(socket, new InformationalError('aborted')) | ||
}) | ||
util.destroy(body) | ||
util.destroy(socket, new InformationalError('aborted')) | ||
} | ||
try { | ||
request.onConnect(abort) | ||
} catch (err) { | ||
errorRequest(client, request, err) | ||
util.errorRequest(client, request, err) | ||
} | ||
if (request.aborted) { | ||
util.destroy(body) | ||
return false | ||
@@ -1008,31 +988,15 @@ } | ||
if (!body || bodyLength === 0) { | ||
if (contentLength === 0) { | ||
socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1') | ||
} else { | ||
assert(contentLength === null, 'no body must not have content length') | ||
socket.write(`${header}\r\n`, 'latin1') | ||
} | ||
request.onRequestSent() | ||
writeBuffer({ abort, body: null, client, request, socket, contentLength, header, expectsPayload }) | ||
} else if (util.isBuffer(body)) { | ||
assert(contentLength === body.byteLength, 'buffer body must have content length') | ||
socket.cork() | ||
socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') | ||
socket.write(body) | ||
socket.uncork() | ||
request.onBodySent(body) | ||
request.onRequestSent() | ||
if (!expectsPayload) { | ||
socket[kReset] = true | ||
} | ||
writeBuffer({ abort, body, client, request, socket, contentLength, header, expectsPayload }) | ||
} else if (util.isBlobLike(body)) { | ||
if (typeof body.stream === 'function') { | ||
writeIterable({ body: body.stream(), client, request, socket, contentLength, header, expectsPayload }) | ||
writeIterable({ abort, body: body.stream(), client, request, socket, contentLength, header, expectsPayload }) | ||
} else { | ||
writeBlob({ body, client, request, socket, contentLength, header, expectsPayload }) | ||
writeBlob({ abort, body, client, request, socket, contentLength, header, expectsPayload }) | ||
} | ||
} else if (util.isStream(body)) { | ||
writeStream({ body, client, request, socket, contentLength, header, expectsPayload }) | ||
writeStream({ abort, body, client, request, socket, contentLength, header, expectsPayload }) | ||
} else if (util.isIterable(body)) { | ||
writeIterable({ body, client, request, socket, contentLength, header, expectsPayload }) | ||
writeIterable({ abort, body, client, request, socket, contentLength, header, expectsPayload }) | ||
} else { | ||
@@ -1045,3 +1009,3 @@ assert(false) | ||
function writeStream ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { | ||
function writeStream ({ abort, body, client, request, socket, contentLength, header, expectsPayload }) { | ||
assert(contentLength !== 0 || client[kRunning] === 0, 'stream body cannot be pipelined') | ||
@@ -1051,3 +1015,3 @@ | ||
const writer = new AsyncWriter({ socket, request, contentLength, client, expectsPayload, header }) | ||
const writer = new AsyncWriter({ abort, socket, request, contentLength, client, expectsPayload, header }) | ||
@@ -1150,3 +1114,33 @@ const onData = function (chunk) { | ||
async function writeBlob ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { | ||
async function writeBuffer ({ abort, body, client, request, socket, contentLength, header, expectsPayload }) { | ||
try { | ||
if (!body) { | ||
if (contentLength === 0) { | ||
socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1') | ||
} else { | ||
assert(contentLength === null, 'no body must not have content length') | ||
socket.write(`${header}\r\n`, 'latin1') | ||
} | ||
} else if (util.isBuffer(body)) { | ||
assert(contentLength === body.byteLength, 'buffer body must have content length') | ||
socket.cork() | ||
socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') | ||
socket.write(body) | ||
socket.uncork() | ||
request.onBodySent(body) | ||
if (!expectsPayload) { | ||
socket[kReset] = true | ||
} | ||
} | ||
request.onRequestSent() | ||
client[kResume]() | ||
} catch (err) { | ||
abort(err) | ||
} | ||
} | ||
async function writeBlob ({ abort, body, client, request, socket, contentLength, header, expectsPayload }) { | ||
assert(contentLength === body.size, 'blob body must have content length') | ||
@@ -1175,7 +1169,7 @@ | ||
} catch (err) { | ||
util.destroy(socket, err) | ||
abort(err) | ||
} | ||
} | ||
async function writeIterable ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { | ||
async function writeIterable ({ abort, body, client, request, socket, contentLength, header, expectsPayload }) { | ||
assert(contentLength !== 0 || client[kRunning] === 0, 'iterator body cannot be pipelined') | ||
@@ -1206,3 +1200,3 @@ | ||
const writer = new AsyncWriter({ socket, request, contentLength, client, expectsPayload, header }) | ||
const writer = new AsyncWriter({ abort, socket, request, contentLength, client, expectsPayload, header }) | ||
try { | ||
@@ -1231,3 +1225,3 @@ // It's up to the user to somehow abort the async iterable. | ||
class AsyncWriter { | ||
constructor ({ socket, request, contentLength, client, expectsPayload, header }) { | ||
constructor ({ abort, socket, request, contentLength, client, expectsPayload, header }) { | ||
this.socket = socket | ||
@@ -1240,2 +1234,3 @@ this.request = request | ||
this.header = header | ||
this.abort = abort | ||
@@ -1356,3 +1351,3 @@ socket[kWriting] = true | ||
destroy (err) { | ||
const { socket, client } = this | ||
const { socket, client, abort } = this | ||
@@ -1363,3 +1358,3 @@ socket[kWriting] = false | ||
assert(client[kRunning] <= 1, 'pipeline should only contain this request') | ||
util.destroy(socket, err) | ||
abort(err) | ||
} | ||
@@ -1366,0 +1361,0 @@ } |
@@ -25,3 +25,2 @@ 'use strict' | ||
kOnError, | ||
// HTTP2 | ||
kMaxConcurrentStreams, | ||
@@ -59,10 +58,16 @@ kHTTP2Session, | ||
function parseH2Headers (headers) { | ||
// set-cookie is always an array. Duplicates are added to the array. | ||
// For duplicate cookie headers, the values are joined together with '; '. | ||
headers = Object.entries(headers).flat(2) | ||
const result = [] | ||
for (const header of headers) { | ||
result.push(Buffer.from(header)) | ||
for (const [name, value] of Object.entries(headers)) { | ||
// h2 may concat the header value by array | ||
// e.g. Set-Cookie | ||
if (Array.isArray(value)) { | ||
for (const subvalue of value) { | ||
// we need to provide each header value of header name | ||
// because the headers handler expect name-value pair | ||
result.push(Buffer.from(name), Buffer.from(subvalue)) | ||
} | ||
} else { | ||
result.push(Buffer.from(name), Buffer.from(value)) | ||
} | ||
} | ||
@@ -91,12 +96,14 @@ | ||
session[kSocket] = socket | ||
session.on('error', onHttp2SessionError) | ||
session.on('frameError', onHttp2FrameError) | ||
session.on('end', onHttp2SessionEnd) | ||
session.on('goaway', onHTTP2GoAway) | ||
session.on('close', function () { | ||
util.addListener(session, 'error', onHttp2SessionError) | ||
util.addListener(session, 'frameError', onHttp2FrameError) | ||
util.addListener(session, 'end', onHttp2SessionEnd) | ||
util.addListener(session, 'goaway', onHTTP2GoAway) | ||
util.addListener(session, 'close', function () { | ||
const { [kClient]: client } = this | ||
const err = this[kError] || new SocketError('closed', util.getSocketInfo(this)) | ||
const err = this[kSocket][kError] || new SocketError('closed', util.getSocketInfo(this)) | ||
client[kSocket] = null | ||
client[kHTTP2Session] = null | ||
@@ -109,3 +116,3 @@ assert(client[kPending] === 0) | ||
const request = requests[i] | ||
errorRequest(client, request, err) | ||
util.errorRequest(client, request, err) | ||
} | ||
@@ -121,2 +128,3 @@ | ||
}) | ||
session.unref() | ||
@@ -127,3 +135,3 @@ | ||
socket.on('error', function (err) { | ||
util.addListener(socket, 'error', function (err) { | ||
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') | ||
@@ -135,3 +143,4 @@ | ||
}) | ||
socket.on('end', function () { | ||
util.addListener(socket, 'end', function () { | ||
util.destroy(this, new SocketError('other side closed', util.getSocketInfo(this))) | ||
@@ -176,3 +185,2 @@ }) | ||
this[kSocket][kError] = err | ||
this[kClient][kOnError](err) | ||
@@ -182,5 +190,4 @@ } | ||
function onHttp2FrameError (type, code, id) { | ||
const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`) | ||
if (id === 0) { | ||
const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`) | ||
this[kSocket][kError] = err | ||
@@ -192,51 +199,28 @@ this[kClient][kOnError](err) | ||
function onHttp2SessionEnd () { | ||
this.destroy(new SocketError('other side closed')) | ||
util.destroy(this[kSocket], new SocketError('other side closed')) | ||
const err = new SocketError('other side closed', util.getSocketInfo(this[kSocket])) | ||
this.destroy(err) | ||
util.destroy(this[kSocket], err) | ||
} | ||
/** | ||
* This is the root cause of #3011 | ||
* We need to handle GOAWAY frames properly, and trigger the session close | ||
* along with the socket right away | ||
* Find a way to trigger the close cycle from here on. | ||
*/ | ||
function onHTTP2GoAway (code) { | ||
const client = this[kClient] | ||
const err = new InformationalError(`HTTP/2: "GOAWAY" frame received with code ${code}`) | ||
client[kSocket] = null | ||
client[kHTTP2Session] = null | ||
if (client.destroyed) { | ||
assert(this[kPending] === 0) | ||
// We need to trigger the close cycle right away | ||
// We need to destroy the session and the socket | ||
// Requests should be failed with the error after the current one is handled | ||
this[kSocket][kError] = err | ||
this[kClient][kOnError](err) | ||
// Fail entire queue. | ||
const requests = client[kQueue].splice(client[kRunningIdx]) | ||
for (let i = 0; i < requests.length; i++) { | ||
const request = requests[i] | ||
errorRequest(this, request, err) | ||
} | ||
} else if (client[kRunning] > 0) { | ||
// Fail head of pipeline. | ||
const request = client[kQueue][client[kRunningIdx]] | ||
client[kQueue][client[kRunningIdx]++] = null | ||
errorRequest(client, request, err) | ||
} | ||
client[kPendingIdx] = client[kRunningIdx] | ||
assert(client[kRunning] === 0) | ||
client.emit('disconnect', | ||
client[kUrl], | ||
[client], | ||
err | ||
) | ||
client[kResume]() | ||
this.unref() | ||
// We send the GOAWAY frame response as no error | ||
this.destroy() | ||
util.destroy(this[kSocket], err) | ||
} | ||
function errorRequest (client, request, err) { | ||
try { | ||
request.onError(err) | ||
assert(request.aborted) | ||
} catch (err) { | ||
client.emit('error', err) | ||
} | ||
} | ||
// https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2 | ||
@@ -252,3 +236,3 @@ function shouldSendContentLength (method) { | ||
if (upgrade) { | ||
errorRequest(client, request, new Error('Upgrade not supported for H2')) | ||
util.errorRequest(client, request, new Error('Upgrade not supported for H2')) | ||
return false | ||
@@ -306,6 +290,6 @@ } | ||
errorRequest(client, request, err) | ||
util.errorRequest(client, request, err) | ||
}) | ||
} catch (err) { | ||
errorRequest(client, request, err) | ||
util.errorRequest(client, request, err) | ||
} | ||
@@ -385,3 +369,3 @@ | ||
if (client[kStrictContentLength]) { | ||
errorRequest(client, request, new RequestContentLengthMismatchError()) | ||
util.errorRequest(client, request, new RequestContentLengthMismatchError()) | ||
return false | ||
@@ -428,3 +412,3 @@ } | ||
const err = new RequestAbortedError() | ||
errorRequest(client, request, err) | ||
util.errorRequest(client, request, err) | ||
util.destroy(stream, err) | ||
@@ -463,3 +447,3 @@ return | ||
const err = new InformationalError('HTTP/2: stream half-closed (remote)') | ||
errorRequest(client, request, err) | ||
util.errorRequest(client, request, err) | ||
util.destroy(stream, err) | ||
@@ -470,3 +454,2 @@ }) | ||
session[kOpenStreams] -= 1 | ||
// TODO(HTTP/2): unref only if current streams count is 0 | ||
if (session[kOpenStreams] === 0) { | ||
@@ -480,2 +463,3 @@ session.unref() | ||
session[kOpenStreams] -= 1 | ||
util.errorRequest(client, request, err) | ||
util.destroy(stream, err) | ||
@@ -487,3 +471,3 @@ } | ||
const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`) | ||
errorRequest(client, request, err) | ||
util.errorRequest(client, request, err) | ||
@@ -490,0 +474,0 @@ if (client[kHTTP2Session] && !client[kHTTP2Session].destroyed && !this.closed && !this.destroyed) { |
@@ -341,3 +341,3 @@ // @ts-check | ||
const request = requests[i] | ||
errorRequest(this, request, err) | ||
util.errorRequest(this, request, err) | ||
} | ||
@@ -382,3 +382,3 @@ | ||
const request = requests[i] | ||
errorRequest(client, request, err) | ||
util.errorRequest(client, request, err) | ||
} | ||
@@ -507,3 +507,3 @@ assert(client[kSize] === 0) | ||
const request = client[kQueue][client[kPendingIdx]++] | ||
errorRequest(client, request, err) | ||
util.errorRequest(client, request, err) | ||
} | ||
@@ -587,3 +587,6 @@ } else { | ||
client[kServerName] = request.servername | ||
client[kHTTPContext]?.destroy(new InformationalError('servername changed')) | ||
client[kHTTPContext]?.destroy(new InformationalError('servername changed'), () => { | ||
client[kHTTPContext] = null | ||
resume(client) | ||
}) | ||
} | ||
@@ -616,11 +619,2 @@ | ||
function errorRequest (client, request, err) { | ||
try { | ||
request.onError(err) | ||
assert(request.aborted) | ||
} catch (err) { | ||
client.emit('error', err) | ||
} | ||
} | ||
module.exports = Client |
@@ -67,5 +67,5 @@ 'use strict' | ||
connect: async (opts, callback) => { | ||
let requestedHost = opts.host | ||
let requestedPath = opts.host | ||
if (!opts.port) { | ||
requestedHost += `:${defaultProtocolPort(opts.protocol)}` | ||
requestedPath += `:${defaultProtocolPort(opts.protocol)}` | ||
} | ||
@@ -76,7 +76,7 @@ try { | ||
port, | ||
path: requestedHost, | ||
path: requestedPath, | ||
signal: opts.signal, | ||
headers: { | ||
...this[kProxyHeaders], | ||
host: requestedHost | ||
host: opts.host | ||
}, | ||
@@ -113,12 +113,14 @@ servername: this[kProxyTls]?.servername || proxyHostname | ||
dispatch (opts, handler) { | ||
const { host } = new URL(opts.origin) | ||
const headers = buildHeaders(opts.headers) | ||
throwIfProxyAuthIsSent(headers) | ||
if (headers && !('host' in headers) && !('Host' in headers)) { | ||
const { host } = new URL(opts.origin) | ||
headers.host = host | ||
} | ||
return this[kAgent].dispatch( | ||
{ | ||
...opts, | ||
headers: { | ||
...headers, | ||
host | ||
} | ||
headers | ||
}, | ||
@@ -125,0 +127,0 @@ handler |
@@ -93,3 +93,3 @@ 'use strict' | ||
createMockScopeDispatchData (statusCode, data, responseOptions = {}) { | ||
createMockScopeDispatchData ({ statusCode, data, responseOptions }) { | ||
const responseData = getResponseData(data) | ||
@@ -103,10 +103,7 @@ const contentLength = this[kContentLength] ? { 'content-length': responseData.length } : {} | ||
validateReplyParameters (statusCode, data, responseOptions) { | ||
if (typeof statusCode === 'undefined') { | ||
validateReplyParameters (replyParameters) { | ||
if (typeof replyParameters.statusCode === 'undefined') { | ||
throw new InvalidArgumentError('statusCode must be defined') | ||
} | ||
if (typeof data === 'undefined') { | ||
throw new InvalidArgumentError('data must be defined') | ||
} | ||
if (typeof responseOptions !== 'object' || responseOptions === null) { | ||
if (typeof replyParameters.responseOptions !== 'object' || replyParameters.responseOptions === null) { | ||
throw new InvalidArgumentError('responseOptions must be an object') | ||
@@ -119,6 +116,6 @@ } | ||
*/ | ||
reply (replyData) { | ||
reply (replyOptionsCallbackOrStatusCode) { | ||
// Values of reply aren't available right now as they | ||
// can only be available when the reply callback is invoked. | ||
if (typeof replyData === 'function') { | ||
if (typeof replyOptionsCallbackOrStatusCode === 'function') { | ||
// We'll first wrap the provided callback in another function, | ||
@@ -129,15 +126,15 @@ // this function will properly resolve the data from the callback | ||
// Our reply options callback contains the parameter for statusCode, data and options. | ||
const resolvedData = replyData(opts) | ||
const resolvedData = replyOptionsCallbackOrStatusCode(opts) | ||
// Check if it is in the right format | ||
if (typeof resolvedData !== 'object') { | ||
if (typeof resolvedData !== 'object' || resolvedData === null) { | ||
throw new InvalidArgumentError('reply options callback must return an object') | ||
} | ||
const { statusCode, data = '', responseOptions = {} } = resolvedData | ||
this.validateReplyParameters(statusCode, data, responseOptions) | ||
const replyParameters = { data: '', responseOptions: {}, ...resolvedData } | ||
this.validateReplyParameters(replyParameters) | ||
// Since the values can be obtained immediately we return them | ||
// from this higher order function that will be resolved later. | ||
return { | ||
...this.createMockScopeDispatchData(statusCode, data, responseOptions) | ||
...this.createMockScopeDispatchData(replyParameters) | ||
} | ||
@@ -155,7 +152,11 @@ } | ||
// just be the statusCode. | ||
const [statusCode, data = '', responseOptions = {}] = [...arguments] | ||
this.validateReplyParameters(statusCode, data, responseOptions) | ||
const replyParameters = { | ||
statusCode: replyOptionsCallbackOrStatusCode, | ||
data: arguments[1] === undefined ? '' : arguments[1], | ||
responseOptions: arguments[2] === undefined ? {} : arguments[2] | ||
} | ||
this.validateReplyParameters(replyParameters) | ||
// Send in-already provided data like usual | ||
const dispatchData = this.createMockScopeDispatchData(statusCode, data, responseOptions) | ||
const dispatchData = this.createMockScopeDispatchData(replyParameters) | ||
const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], dispatchData) | ||
@@ -162,0 +163,0 @@ return new MockScope(newMockDispatch) |
@@ -11,3 +11,3 @@ 'use strict' | ||
} = require('./mock-symbols') | ||
const { buildURL, nop } = require('../core/util') | ||
const { buildURL } = require('../core/util') | ||
const { STATUS_CODES } = require('node:http') | ||
@@ -289,6 +289,6 @@ const { | ||
handler.abort = nop | ||
handler.onHeaders(statusCode, responseHeaders, resume, getStatusText(statusCode)) | ||
handler.onData(Buffer.from(responseData)) | ||
handler.onComplete(responseTrailers) | ||
handler.onConnect?.(err => handler.onError(err), null) | ||
handler.onHeaders?.(statusCode, responseHeaders, resume, getStatusText(statusCode)) | ||
handler.onData?.(Buffer.from(responseData)) | ||
handler.onComplete?.(responseTrailers) | ||
deleteMockDispatch(mockDispatches, key) | ||
@@ -295,0 +295,0 @@ } |
@@ -17,4 +17,4 @@ 'use strict' | ||
'540', '548', '554', '556', '563', '587', '601', '636', '989', '990', '993', '995', '1719', '1720', '1723', | ||
'2049', '3659', '4045', '5060', '5061', '6000', '6566', '6665', '6666', '6667', '6668', '6669', '6697', | ||
'10080' | ||
'2049', '3659', '4045', '4190', '5060', '5061', '6000', '6566', '6665', '6666', '6667', '6668', '6669', '6679', | ||
'6697', '10080' | ||
] | ||
@@ -21,0 +21,0 @@ |
@@ -7,3 +7,3 @@ 'use strict' | ||
const { channels } = require('../../core/diagnostics') | ||
const { isValidStatusCode, failWebsocketConnection, websocketMessageReceived } = require('./util') | ||
const { isValidStatusCode, failWebsocketConnection, websocketMessageReceived, utf8Decode } = require('./util') | ||
const { WebsocketFrameSend } = require('./frame') | ||
@@ -318,4 +318,3 @@ | ||
try { | ||
// TODO: optimize this | ||
reason = new TextDecoder('utf-8', { fatal: true }).decode(reason) | ||
reason = utf8Decode(reason) | ||
} catch { | ||
@@ -322,0 +321,0 @@ return null |
@@ -6,2 +6,3 @@ 'use strict' | ||
const { MessageEvent, ErrorEvent } = require('./events') | ||
const { isUtf8 } = require('node:buffer') | ||
@@ -91,3 +92,3 @@ /* globals Blob */ | ||
try { | ||
dataForEvent = new TextDecoder('utf-8', { fatal: true }).decode(data) | ||
dataForEvent = utf8Decode(data) | ||
} catch { | ||
@@ -205,2 +206,29 @@ failWebsocketConnection(ws, 'Received invalid UTF-8 in text frame.') | ||
// https://nodejs.org/api/intl.html#detecting-internationalization-support | ||
const hasIntl = typeof process.versions.icu === 'string' | ||
const fatalDecoder = hasIntl ? new TextDecoder('utf-8', { fatal: true }) : undefined | ||
/** | ||
* Converts a Buffer to utf-8, even on platforms without icu. | ||
* @param {Buffer} buffer | ||
*/ | ||
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) { | ||
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.') | ||
} | ||
return buffer.toString('utf-8') | ||
} | ||
} | ||
module.exports = { | ||
@@ -215,3 +243,4 @@ isConnecting, | ||
failWebsocketConnection, | ||
websocketMessageReceived | ||
websocketMessageReceived, | ||
utf8Decode | ||
} |
{ | ||
"name": "undici", | ||
"version": "6.11.1", | ||
"version": "6.12.0", | ||
"description": "An HTTP/1.1 client, written from scratch for Node.js", | ||
@@ -71,12 +71,16 @@ "homepage": "https://undici.nodejs.org", | ||
"test": "npm run test:javascript && cross-env NODE_V8_COVERAGE= npm run test:typescript", | ||
"test:javascript": "node scripts/generate-pem && npm run test:unit && npm run test:node-fetch && npm run test:fetch && npm run test:cookies && npm run test:eventsource && npm run test:wpt && npm run test:websocket && npm run test:node-test && npm run test:jest", | ||
"test:javascript:withoutintl": "node scripts/generate-pem && npm run test:unit && npm run test:node-fetch && npm run test:fetch:nobuild && npm run test:cookies && npm run test:eventsource:nobuild && npm run test:wpt:withoutintl && npm run test:node-test", | ||
"test:javascript": "node scripts/generate-pem && npm run test:unit && npm run test:node-fetch && npm run test:cache && npm run test:interceptors && npm run test:fetch && npm run test:cookies && npm run test:eventsource && npm run test:wpt && npm run test:websocket && npm run test:node-test && npm run test:jest", | ||
"test:javascript:withoutintl": "node scripts/generate-pem && npm run test:unit && npm run test:node-fetch && npm run test:fetch:nobuild && npm run test:cache && npm run test:interceptors && npm run test:cookies && npm run test:eventsource:nobuild && npm run test:wpt:withoutintl && npm run test:node-test", | ||
"test:busboy": "borp -p \"test/busboy/*.js\"", | ||
"test:cache": "borp -p \"test/cache/*.js\"", | ||
"test:cookies": "borp -p \"test/cookie/*.js\"", | ||
"test:node-fetch": "borp -p \"test/node-fetch/**/*.js\"", | ||
"test:eventsource": "npm run build:node && npm run test:eventsource:nobuild", | ||
"test:eventsource:nobuild": "borp --expose-gc -p \"test/eventsource/*.js\"", | ||
"test:fuzzing": "node test/fuzzing/fuzzing.test.js", | ||
"test:fetch": "npm run build:node && npm run test:fetch:nobuild", | ||
"test:fetch:nobuild": "borp --expose-gc -p \"test/fetch/*.js\" && borp -p \"test/webidl/*.js\" && borp -p \"test/busboy/*.js\"", | ||
"test:fetch:nobuild": "borp --expose-gc -p \"test/fetch/*.js\" && npm run test:webidl && npm run test:busboy", | ||
"test:interceptors": "borp -p \"test/interceptors/*.js\"", | ||
"test:jest": "cross-env NODE_V8_COVERAGE= jest", | ||
"test:unit": "borp --expose-gc -p \"test/*.js\"", | ||
"test:node-fetch": "borp -p \"test/node-fetch/**/*.js\"", | ||
"test:node-test": "borp -p \"test/node-test/**/*.js\"", | ||
@@ -86,2 +90,3 @@ "test:tdd": "borp --expose-gc -p \"test/*.js\"", | ||
"test:typescript": "tsd && tsc --skipLibCheck test/imports/undici-import.ts", | ||
"test:webidl": "borp -p \"test/webidl/*.js\"", | ||
"test:websocket": "borp -p \"test/websocket/*.js\"", | ||
@@ -97,4 +102,3 @@ "test:wpt": "node test/wpt/start-fetch.mjs && node test/wpt/start-FileAPI.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-websockets.mjs && node test/wpt/start-cacheStorage.mjs && node test/wpt/start-eventsource.mjs", | ||
"serve:website": "echo \"Error: Documentation has been moved to '/docs'\" && exit 1", | ||
"prepare": "husky install && node ./scripts/platform-shell.js", | ||
"fuzz": "jsfuzz test/fuzzing/fuzz.js corpus" | ||
"prepare": "husky install && node ./scripts/platform-shell.js" | ||
}, | ||
@@ -110,2 +114,3 @@ "devDependencies": { | ||
"dns-packet": "^5.4.0", | ||
"fast-check": "^3.17.1", | ||
"form-data": "^4.0.0", | ||
@@ -117,3 +122,2 @@ "formdata-node": "^6.0.3", | ||
"jsdom": "^24.0.0", | ||
"jsfuzz": "^1.0.15", | ||
"node-forge": "^1.3.1", | ||
@@ -120,0 +124,0 @@ "pre-commit": "^1.2.2", |
@@ -228,3 +228,3 @@ import { URL } from 'url' | ||
/** Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. */ | ||
onHeaders?(statusCode: number, headers: Buffer[] | string[] | null, resume: () => void, statusText: string): boolean; | ||
onHeaders?(statusCode: number, headers: Buffer[], resume: () => void, statusText: string): boolean; | ||
/** Invoked when response payload data is received. */ | ||
@@ -231,0 +231,0 @@ onData?(chunk: Buffer): boolean; |
@@ -166,3 +166,3 @@ // based on https://github.com/Ethan-Arrowood/undici-fetch/blob/249269714db874351589d2d364a0645d5160ae71/index.d.ts (MIT license) | ||
readonly redirect: RequestRedirect | ||
readonly referrerPolicy: string | ||
readonly referrerPolicy: ReferrerPolicy | ||
readonly url: string | ||
@@ -169,0 +169,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1133282
23573
0
55