Comparing version 5.20.0 to 5.22.1
@@ -10,15 +10,21 @@ # Errors | ||
| Error | Error Codes | Description | | ||
| ------------------------------------ | ------------------------------------- | -------------------------------------------------- | | ||
| `InvalidArgumentError` | `UND_ERR_INVALID_ARG` | passed an invalid argument. | | ||
| `InvalidReturnValueError` | `UND_ERR_INVALID_RETURN_VALUE` | returned an invalid value. | | ||
| `RequestAbortedError` | `UND_ERR_ABORTED` | the request has been aborted by the user | | ||
| `ClientDestroyedError` | `UND_ERR_DESTROYED` | trying to use a destroyed client. | | ||
| `ClientClosedError` | `UND_ERR_CLOSED` | trying to use a closed client. | | ||
| `SocketError` | `UND_ERR_SOCKET` | there is an error with the socket. | | ||
| `NotSupportedError` | `UND_ERR_NOT_SUPPORTED` | encountered unsupported functionality. | | ||
| `RequestContentLengthMismatchError` | `UND_ERR_REQ_CONTENT_LENGTH_MISMATCH` | request body does not match content-length header | | ||
| `ResponseContentLengthMismatchError` | `UND_ERR_RES_CONTENT_LENGTH_MISMATCH` | response body does not match content-length header | | ||
| `InformationalError` | `UND_ERR_INFO` | expected error with reason | | ||
| `ResponseExceededMaxSizeError` | `UND_ERR_RES_EXCEEDED_MAX_SIZE` | response body exceed the max size allowed | | ||
| Error | Error Codes | Description | | ||
| ------------------------------------ | ------------------------------------- | ------------------------------------------------------------------------- | | ||
| `UndiciError` | `UND_ERR` | all errors below are extended from `UndiciError`. | | ||
| `ConnectTimeoutError` | `UND_ERR_CONNECT_TIMEOUT` | socket is destroyed due to connect timeout. | | ||
| `HeadersTimeoutError` | `UND_ERR_HEADERS_TIMEOUT` | socket is destroyed due to headers timeout. | | ||
| `HeadersOverflowError` | `UND_ERR_HEADERS_OVERFLOW` | socket is destroyed due to headers' max size being exceeded. | | ||
| `BodyTimeoutError` | `UND_ERR_BODY_TIMEOUT` | socket is destroyed due to body timeout. | | ||
| `ResponseStatusCodeError` | `UND_ERR_RESPONSE_STATUS_CODE` | an error is thrown when `throwOnError` is `true` for status codes >= 400. | | ||
| `InvalidArgumentError` | `UND_ERR_INVALID_ARG` | passed an invalid argument. | | ||
| `InvalidReturnValueError` | `UND_ERR_INVALID_RETURN_VALUE` | returned an invalid value. | | ||
| `RequestAbortedError` | `UND_ERR_ABORTED` | the request has been aborted by the user | | ||
| `ClientDestroyedError` | `UND_ERR_DESTROYED` | trying to use a destroyed client. | | ||
| `ClientClosedError` | `UND_ERR_CLOSED` | trying to use a closed client. | | ||
| `SocketError` | `UND_ERR_SOCKET` | there is an error with the socket. | | ||
| `NotSupportedError` | `UND_ERR_NOT_SUPPORTED` | encountered unsupported functionality. | | ||
| `RequestContentLengthMismatchError` | `UND_ERR_REQ_CONTENT_LENGTH_MISMATCH` | request body does not match content-length header | | ||
| `ResponseContentLengthMismatchError` | `UND_ERR_RES_CONTENT_LENGTH_MISMATCH` | response body does not match content-length header | | ||
| `InformationalError` | `UND_ERR_INFO` | expected error with reason | | ||
| `ResponseExceededMaxSizeError` | `UND_ERR_RES_EXCEEDED_MAX_SIZE` | response body exceed the max size allowed | | ||
@@ -25,0 +31,0 @@ ### `SocketError` |
@@ -11,2 +11,4 @@ # Fetch | ||
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 | ||
@@ -13,0 +15,0 @@ |
@@ -22,2 +22,3 @@ # Class: ProxyAgent | ||
* **auth** `string` (**deprecated**) - Use token. | ||
* **clientFactory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Pool(origin, opts)` | ||
@@ -87,3 +88,4 @@ Examples: | ||
uri: 'my.proxy.server', | ||
token: 'Bearer xxxx' | ||
// token: 'Bearer xxxx' | ||
token: `Basic ${Buffer.from('username:password').toString('base64')}` | ||
}); | ||
@@ -90,0 +92,0 @@ setGlobalDispatcher(proxyAgent); |
# Class: WebSocket | ||
> ⚠️ Warning: the WebSocket API is experimental and has known bugs. | ||
> ⚠️ Warning: the WebSocket API is experimental. | ||
Extends: [`EventTarget`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) | ||
The WebSocket object provides a way to manage a WebSocket connection to a server, allowing bidirectional communication. The API follows the [WebSocket spec](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket). | ||
The WebSocket object provides a way to manage a WebSocket connection to a server, allowing bidirectional communication. The API follows the [WebSocket spec](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) and [RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455). | ||
@@ -14,4 +14,27 @@ ## `new WebSocket(url[, protocol])` | ||
* **url** `URL | string` - The url's protocol *must* be `ws` or `wss`. | ||
* **protocol** `string | string[]` (optional) - Subprotocol(s) to request the server use. | ||
* **protocol** `string | string[] | WebSocketInit` (optional) - Subprotocol(s) to request the server use, or a [`Dispatcher`](./Dispatcher.md). | ||
### Example: | ||
This example will not work in browsers or other platforms that don't allow passing an object. | ||
```mjs | ||
import { WebSocket, ProxyAgent } from 'undici' | ||
const proxyAgent = new ProxyAgent('my.proxy.server') | ||
const ws = new WebSocket('wss://echo.websocket.events', { | ||
dispatcher: proxyAgent, | ||
protocols: ['echo', 'chat'] | ||
}) | ||
``` | ||
If you do not need a custom Dispatcher, it's recommended to use the following pattern: | ||
```mjs | ||
import { WebSocket } from 'undici' | ||
const ws = new WebSocket('wss://echo.websocket.events', ['echo', 'chat']) | ||
``` | ||
## Read More | ||
@@ -18,0 +41,0 @@ |
@@ -27,2 +27,3 @@ import Dispatcher from'./types/dispatcher' | ||
export * from './types/content-type' | ||
export * from './types/cache' | ||
export { Interceptable } from './types/mock-interceptor' | ||
@@ -56,2 +57,3 @@ | ||
var fetch: typeof import('./types/fetch').fetch; | ||
var caches: typeof import('./types/cache').caches; | ||
} |
@@ -124,2 +124,9 @@ 'use strict' | ||
module.exports.getGlobalOrigin = getGlobalOrigin | ||
const { CacheStorage } = require('./lib/cache/cachestorage') | ||
const { kConstruct } = require('./lib/cache/symbols') | ||
// Cache & CacheStorage are tightly coupled with fetch. Even if it may run | ||
// in an older version of Node, it doesn't have any use without fetch. | ||
module.exports.caches = new CacheStorage(kConstruct) | ||
} | ||
@@ -126,0 +133,0 @@ |
@@ -6,6 +6,6 @@ 'use strict' | ||
InvalidArgumentError, | ||
RequestAbortedError, | ||
ResponseStatusCodeError | ||
RequestAbortedError | ||
} = require('../core/errors') | ||
const util = require('../core/util') | ||
const { getResolveErrorBodyCallback } = require('./util') | ||
const { AsyncResource } = require('async_hooks') | ||
@@ -20,3 +20,3 @@ const { addSignal, removeSignal } = require('./abort-signal') | ||
const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError } = opts | ||
const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError, highWaterMark } = opts | ||
@@ -28,2 +28,6 @@ try { | ||
if (highWaterMark && (typeof highWaterMark !== 'number' || highWaterMark < 0)) { | ||
throw new InvalidArgumentError('invalid highWaterMark') | ||
} | ||
if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { | ||
@@ -59,2 +63,3 @@ throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') | ||
this.throwOnError = throwOnError | ||
this.highWaterMark = highWaterMark | ||
@@ -80,7 +85,8 @@ if (util.isStream(body)) { | ||
onHeaders (statusCode, rawHeaders, resume, statusMessage) { | ||
const { callback, opaque, abort, context } = this | ||
const { callback, opaque, abort, context, responseHeaders, highWaterMark } = this | ||
const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) | ||
if (statusCode < 200) { | ||
if (this.onInfo) { | ||
const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) | ||
this.onInfo({ statusCode, headers }) | ||
@@ -91,9 +97,8 @@ } | ||
const parsedHeaders = util.parseHeaders(rawHeaders) | ||
const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers | ||
const contentType = parsedHeaders['content-type'] | ||
const body = new Readable(resume, abort, contentType) | ||
const body = new Readable({ resume, abort, contentType, highWaterMark }) | ||
this.callback = null | ||
this.res = body | ||
const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) | ||
@@ -105,13 +110,12 @@ if (callback !== null) { | ||
) | ||
return | ||
} else { | ||
this.runInAsyncScope(callback, null, null, { | ||
statusCode, | ||
headers, | ||
trailers: this.trailers, | ||
opaque, | ||
body, | ||
context | ||
}) | ||
} | ||
this.runInAsyncScope(callback, null, null, { | ||
statusCode, | ||
headers, | ||
trailers: this.trailers, | ||
opaque, | ||
body, | ||
context | ||
}) | ||
} | ||
@@ -163,29 +167,2 @@ } | ||
async function getResolveErrorBodyCallback ({ callback, body, contentType, statusCode, statusMessage, headers }) { | ||
if (statusCode === 204 || !contentType) { | ||
body.dump() | ||
process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers)) | ||
return | ||
} | ||
try { | ||
if (contentType.startsWith('application/json')) { | ||
const payload = await body.json() | ||
process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload)) | ||
return | ||
} | ||
if (contentType.startsWith('text/')) { | ||
const payload = await body.text() | ||
process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload)) | ||
return | ||
} | ||
} catch (err) { | ||
// Process in a fallback if error | ||
} | ||
body.dump() | ||
process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers)) | ||
} | ||
function request (opts, callback) { | ||
@@ -192,0 +169,0 @@ if (callback === undefined) { |
'use strict' | ||
const { finished } = require('stream') | ||
const { finished, PassThrough } = require('stream') | ||
const { | ||
@@ -10,2 +10,3 @@ InvalidArgumentError, | ||
const util = require('../core/util') | ||
const { getResolveErrorBodyCallback } = require('./util') | ||
const { AsyncResource } = require('async_hooks') | ||
@@ -20,3 +21,3 @@ const { addSignal, removeSignal } = require('./abort-signal') | ||
const { signal, method, opaque, body, onInfo, responseHeaders } = opts | ||
const { signal, method, opaque, body, onInfo, responseHeaders, throwOnError } = opts | ||
@@ -62,2 +63,3 @@ try { | ||
this.onInfo = onInfo || null | ||
this.throwOnError = throwOnError || false | ||
@@ -82,8 +84,9 @@ if (util.isStream(body)) { | ||
onHeaders (statusCode, rawHeaders, resume) { | ||
const { factory, opaque, context } = this | ||
onHeaders (statusCode, rawHeaders, resume, statusMessage) { | ||
const { factory, opaque, context, callback, responseHeaders } = this | ||
const headers = responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) | ||
if (statusCode < 200) { | ||
if (this.onInfo) { | ||
const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) | ||
this.onInfo({ statusCode, headers }) | ||
@@ -95,37 +98,51 @@ } | ||
this.factory = null | ||
const headers = this.responseHeaders === 'raw' ? util.parseRawHeaders(rawHeaders) : util.parseHeaders(rawHeaders) | ||
const res = this.runInAsyncScope(factory, null, { | ||
statusCode, | ||
headers, | ||
opaque, | ||
context | ||
}) | ||
if ( | ||
!res || | ||
typeof res.write !== 'function' || | ||
typeof res.end !== 'function' || | ||
typeof res.on !== 'function' | ||
) { | ||
throw new InvalidReturnValueError('expected Writable') | ||
} | ||
let res | ||
res.on('drain', resume) | ||
// TODO: Avoid finished. It registers an unnecessary amount of listeners. | ||
finished(res, { readable: false }, (err) => { | ||
const { callback, res, opaque, trailers, abort } = this | ||
if (this.throwOnError && statusCode >= 400) { | ||
const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers | ||
const contentType = parsedHeaders['content-type'] | ||
res = new PassThrough() | ||
this.res = null | ||
if (err || !res.readable) { | ||
util.destroy(res, err) | ||
} | ||
this.callback = null | ||
this.runInAsyncScope(callback, null, err || null, { opaque, trailers }) | ||
this.runInAsyncScope(getResolveErrorBodyCallback, null, | ||
{ callback, body: res, contentType, statusCode, statusMessage, headers } | ||
) | ||
} else { | ||
res = this.runInAsyncScope(factory, null, { | ||
statusCode, | ||
headers, | ||
opaque, | ||
context | ||
}) | ||
if (err) { | ||
abort() | ||
if ( | ||
!res || | ||
typeof res.write !== 'function' || | ||
typeof res.end !== 'function' || | ||
typeof res.on !== 'function' | ||
) { | ||
throw new InvalidReturnValueError('expected Writable') | ||
} | ||
}) | ||
// TODO: Avoid finished. It registers an unnecessary amount of listeners. | ||
finished(res, { readable: false }, (err) => { | ||
const { callback, res, opaque, trailers, abort } = this | ||
this.res = null | ||
if (err || !res.readable) { | ||
util.destroy(res, err) | ||
} | ||
this.callback = null | ||
this.runInAsyncScope(callback, null, err || null, { opaque, trailers }) | ||
if (err) { | ||
abort() | ||
} | ||
}) | ||
} | ||
res.on('drain', resume) | ||
this.res = res | ||
@@ -132,0 +149,0 @@ |
@@ -7,3 +7,3 @@ // Ported from https://github.com/nodejs/undici/pull/907 | ||
const { Readable } = require('stream') | ||
const { RequestAbortedError, NotSupportedError } = require('../core/errors') | ||
const { RequestAbortedError, NotSupportedError, InvalidArgumentError } = require('../core/errors') | ||
const util = require('../core/util') | ||
@@ -21,7 +21,12 @@ const { ReadableStreamFrom, toUSVString } = require('../core/util') | ||
module.exports = class BodyReadable extends Readable { | ||
constructor (resume, abort, contentType = '') { | ||
constructor ({ | ||
resume, | ||
abort, | ||
contentType = '', | ||
highWaterMark = 64 * 1024 // Same as nodejs fs streams. | ||
}) { | ||
super({ | ||
autoDestroy: true, | ||
read: resume, | ||
highWaterMark: 64 * 1024 // Same as nodejs fs streams. | ||
highWaterMark | ||
}) | ||
@@ -151,4 +156,16 @@ | ||
let limit = opts && Number.isFinite(opts.limit) ? opts.limit : 262144 | ||
const signal = opts && opts.signal | ||
const abortFn = () => { | ||
this.destroy() | ||
} | ||
if (signal) { | ||
if (typeof signal !== 'object' || !('aborted' in signal)) { | ||
throw new InvalidArgumentError('signal must be an AbortSignal') | ||
} | ||
util.throwIfAborted(signal) | ||
signal.addEventListener('abort', abortFn, { once: true }) | ||
} | ||
try { | ||
for await (const chunk of this) { | ||
util.throwIfAborted(signal) | ||
limit -= Buffer.byteLength(chunk) | ||
@@ -160,3 +177,7 @@ if (limit < 0) { | ||
} catch { | ||
// Do nothing... | ||
util.throwIfAborted(signal) | ||
} finally { | ||
if (signal) { | ||
signal.removeEventListener('abort', abortFn) | ||
} | ||
} | ||
@@ -163,0 +184,0 @@ } |
@@ -0,1 +1,3 @@ | ||
// @ts-check | ||
'use strict' | ||
@@ -22,3 +24,4 @@ | ||
HTTPParserError, | ||
ResponseExceededMaxSizeError | ||
ResponseExceededMaxSizeError, | ||
ClientDestroyedError | ||
} = require('./core/errors') | ||
@@ -89,3 +92,11 @@ const buildConnector = require('./core/connect') | ||
/** | ||
* @type {import('../types/client').default} | ||
*/ | ||
class Client extends DispatcherBase { | ||
/** | ||
* | ||
* @param {string|URL} url | ||
* @param {import('../types/client').Client.Options} options | ||
*/ | ||
constructor (url, { | ||
@@ -315,3 +326,3 @@ interceptors, | ||
if (!this[kSize]) { | ||
this.destroy(resolve) | ||
resolve(null) | ||
} else { | ||
@@ -333,2 +344,3 @@ this[kClosedResolve] = resolve | ||
if (this[kClosedResolve]) { | ||
// TODO (fix): Should we error here with ClientDestroyedError? | ||
this[kClosedResolve]() | ||
@@ -356,7 +368,7 @@ this[kClosedResolve] = null | ||
async function lazyllhttp () { | ||
const llhttpWasmData = process.env.JEST_WORKER_ID ? require('./llhttp/llhttp.wasm.js') : undefined | ||
const llhttpWasmData = process.env.JEST_WORKER_ID ? require('./llhttp/llhttp-wasm.js') : undefined | ||
let mod | ||
try { | ||
mod = await WebAssembly.compile(Buffer.from(require('./llhttp/llhttp_simd.wasm.js'), 'base64')) | ||
mod = await WebAssembly.compile(Buffer.from(require('./llhttp/llhttp_simd-wasm.js'), 'base64')) | ||
} catch (e) { | ||
@@ -369,3 +381,3 @@ /* istanbul ignore next */ | ||
// got me to remove that check to avoid breaking Node 12. | ||
mod = await WebAssembly.compile(Buffer.from(llhttpWasmData || require('./llhttp/llhttp.wasm.js'), 'base64')) | ||
mod = await WebAssembly.compile(Buffer.from(llhttpWasmData || require('./llhttp/llhttp-wasm.js'), 'base64')) | ||
} | ||
@@ -566,3 +578,6 @@ | ||
const len = new Uint8Array(llhttp.memory.buffer, ptr).indexOf(0) | ||
message = Buffer.from(llhttp.memory.buffer, ptr, len).toString() | ||
message = | ||
'Response does not match the HTTP/1.1 protocol (' + | ||
Buffer.from(llhttp.memory.buffer, ptr, len).toString() + | ||
')' | ||
} | ||
@@ -1082,2 +1097,7 @@ throw new HTTPParserError(message, constants.ERROR[ret], data.slice(offset)) | ||
if (client.destroyed) { | ||
util.destroy(socket.on('error', () => {}), new ClientDestroyedError()) | ||
return | ||
} | ||
if (!llhttpInstance) { | ||
@@ -1125,2 +1145,6 @@ llhttpInstance = await llhttpPromise | ||
} catch (err) { | ||
if (client.destroyed) { | ||
return | ||
} | ||
client[kConnecting] = false | ||
@@ -1188,4 +1212,5 @@ | ||
if (client.closed && !client[kSize]) { | ||
client.destroy() | ||
if (client[kClosedResolve] && !client[kSize]) { | ||
client[kClosedResolve]() | ||
client[kClosedResolve] = null | ||
return | ||
@@ -1442,6 +1467,6 @@ } | ||
if (contentLength === 0) { | ||
socket.write(`${header}content-length: 0\r\n\r\n`, 'ascii') | ||
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`, 'ascii') | ||
socket.write(`${header}\r\n`, 'latin1') | ||
} | ||
@@ -1453,3 +1478,3 @@ request.onRequestSent() | ||
socket.cork() | ||
socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'ascii') | ||
socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') | ||
socket.write(body) | ||
@@ -1487,5 +1512,7 @@ socket.uncork() | ||
const onData = function (chunk) { | ||
if (finished) { | ||
return | ||
} | ||
try { | ||
assert(!finished) | ||
if (!writer.write(chunk) && this.pause) { | ||
@@ -1499,3 +1526,5 @@ this.pause() | ||
const onDrain = function () { | ||
assert(!finished) | ||
if (finished) { | ||
return | ||
} | ||
@@ -1571,3 +1600,3 @@ if (body.resume) { | ||
socket.cork() | ||
socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'ascii') | ||
socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') | ||
socket.write(buffer) | ||
@@ -1676,2 +1705,4 @@ socket.uncork() | ||
socket.cork() | ||
if (bytesWritten === 0) { | ||
@@ -1683,5 +1714,5 @@ if (!expectsPayload) { | ||
if (contentLength === null) { | ||
socket.write(`${header}transfer-encoding: chunked\r\n`, 'ascii') | ||
socket.write(`${header}transfer-encoding: chunked\r\n`, 'latin1') | ||
} else { | ||
socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'ascii') | ||
socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1') | ||
} | ||
@@ -1691,3 +1722,3 @@ } | ||
if (contentLength === null) { | ||
socket.write(`\r\n${len.toString(16)}\r\n`, 'ascii') | ||
socket.write(`\r\n${len.toString(16)}\r\n`, 'latin1') | ||
} | ||
@@ -1699,2 +1730,4 @@ | ||
socket.uncork() | ||
request.onBodySent(chunk) | ||
@@ -1735,8 +1768,8 @@ | ||
socket.write(`${header}content-length: 0\r\n\r\n`, 'ascii') | ||
socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1') | ||
} else { | ||
socket.write(`${header}\r\n`, 'ascii') | ||
socket.write(`${header}\r\n`, 'latin1') | ||
} | ||
} else if (contentLength === null) { | ||
socket.write('\r\n0\r\n\r\n', 'ascii') | ||
socket.write('\r\n0\r\n\r\n', 'latin1') | ||
} | ||
@@ -1743,0 +1776,0 @@ |
@@ -185,2 +185,3 @@ 'use strict' | ||
this.body = bodyStream.stream | ||
this.contentLength = bodyStream.length | ||
} else if (util.isBlobLike(body) && this.contentType == null && body.type) { | ||
@@ -187,0 +188,0 @@ this.contentType = body.type |
@@ -44,3 +44,3 @@ module.exports = { | ||
kOnDestroyed: Symbol('destroy callbacks'), | ||
kPipelining: Symbol('pipelinig'), | ||
kPipelining: Symbol('pipelining'), | ||
kSocket: Symbol('socket'), | ||
@@ -47,0 +47,0 @@ kHostHeader: Symbol('host header'), |
@@ -18,3 +18,3 @@ 'use strict' | ||
function isStream (obj) { | ||
return obj && typeof obj.pipe === 'function' | ||
return obj && typeof obj === 'object' && typeof obj.pipe === 'function' && typeof obj.on === 'function' | ||
} | ||
@@ -50,30 +50,36 @@ | ||
url = new URL(url) | ||
if (!/^https?:/.test(url.origin || url.protocol)) { | ||
throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.') | ||
} | ||
return url | ||
} | ||
if (!url || typeof url !== 'object') { | ||
throw new InvalidArgumentError('invalid url') | ||
throw new InvalidArgumentError('Invalid URL: The URL argument must be a non-null object.') | ||
} | ||
if (url.port != null && url.port !== '' && !Number.isFinite(parseInt(url.port))) { | ||
throw new InvalidArgumentError('invalid port') | ||
throw new InvalidArgumentError('Invalid URL: port must be a valid integer or a string representation of an integer.') | ||
} | ||
if (url.path != null && typeof url.path !== 'string') { | ||
throw new InvalidArgumentError('invalid path') | ||
throw new InvalidArgumentError('Invalid URL path: the path must be a string or null/undefined.') | ||
} | ||
if (url.pathname != null && typeof url.pathname !== 'string') { | ||
throw new InvalidArgumentError('invalid pathname') | ||
throw new InvalidArgumentError('Invalid URL pathname: the pathname must be a string or null/undefined.') | ||
} | ||
if (url.hostname != null && typeof url.hostname !== 'string') { | ||
throw new InvalidArgumentError('invalid hostname') | ||
throw new InvalidArgumentError('Invalid URL hostname: the hostname must be a string or null/undefined.') | ||
} | ||
if (url.origin != null && typeof url.origin !== 'string') { | ||
throw new InvalidArgumentError('invalid origin') | ||
throw new InvalidArgumentError('Invalid URL origin: the origin must be a string or null/undefined.') | ||
} | ||
if (!/^https?:/.test(url.origin || url.protocol)) { | ||
throw new InvalidArgumentError('invalid protocol') | ||
throw new InvalidArgumentError('Invalid URL protocol: the URL must start with `http:` or `https:`.') | ||
} | ||
@@ -221,6 +227,2 @@ | ||
const encoding = key.length === 19 && key === 'content-disposition' | ||
? 'latin1' | ||
: 'utf8' | ||
if (!val) { | ||
@@ -230,3 +232,3 @@ if (Array.isArray(headers[i + 1])) { | ||
} else { | ||
obj[key] = headers[i + 1].toString(encoding) | ||
obj[key] = headers[i + 1].toString('utf8') | ||
} | ||
@@ -238,5 +240,11 @@ } else { | ||
} | ||
val.push(headers[i + 1].toString(encoding)) | ||
val.push(headers[i + 1].toString('utf8')) | ||
} | ||
} | ||
// See https://github.com/nodejs/node/pull/46528 | ||
if ('content-length' in obj && 'content-disposition' in obj) { | ||
obj['content-disposition'] = Buffer.from(obj['content-disposition']).toString('latin1') | ||
} | ||
return obj | ||
@@ -247,13 +255,24 @@ } | ||
const ret = [] | ||
let hasContentLength = false | ||
let contentDispositionIdx = -1 | ||
for (let n = 0; n < headers.length; n += 2) { | ||
const key = headers[n + 0].toString() | ||
const val = headers[n + 1].toString('utf8') | ||
const encoding = key.length === 19 && key.toLowerCase() === 'content-disposition' | ||
? 'latin1' | ||
: 'utf8' | ||
if (key.length === 14 && (key === 'content-length' || key.toLowerCase() === 'content-length')) { | ||
ret.push(key, val) | ||
hasContentLength = true | ||
} else if (key.length === 19 && (key === 'content-disposition' || key.toLowerCase() === 'content-disposition')) { | ||
contentDispositionIdx = ret.push(key, val) - 1 | ||
} else { | ||
ret.push(key, val) | ||
} | ||
} | ||
const val = headers[n + 1].toString(encoding) | ||
// See https://github.com/nodejs/node/pull/46528 | ||
if (hasContentLength && contentDispositionIdx !== -1) { | ||
ret[contentDispositionIdx] = Buffer.from(ret[contentDispositionIdx]).toString('latin1') | ||
} | ||
ret.push(key, val) | ||
} | ||
return ret | ||
@@ -384,19 +403,45 @@ } | ||
// all the required methods. | ||
function isFormDataLike (chunk) { | ||
return (chunk && | ||
chunk.constructor && chunk.constructor.name === 'FormData' && | ||
typeof chunk === 'object' && | ||
(typeof chunk.append === 'function' && | ||
typeof chunk.delete === 'function' && | ||
typeof chunk.get === 'function' && | ||
typeof chunk.getAll === 'function' && | ||
typeof chunk.has === 'function' && | ||
typeof chunk.set === 'function' && | ||
typeof chunk.entries === 'function' && | ||
typeof chunk.keys === 'function' && | ||
typeof chunk.values === 'function' && | ||
typeof chunk.forEach === 'function') | ||
function isFormDataLike (object) { | ||
return ( | ||
object && | ||
typeof object === 'object' && | ||
typeof object.append === 'function' && | ||
typeof object.delete === 'function' && | ||
typeof object.get === 'function' && | ||
typeof object.getAll === 'function' && | ||
typeof object.has === 'function' && | ||
typeof object.set === 'function' && | ||
object[Symbol.toStringTag] === 'FormData' | ||
) | ||
} | ||
function throwIfAborted (signal) { | ||
if (!signal) { return } | ||
if (typeof signal.throwIfAborted === 'function') { | ||
signal.throwIfAborted() | ||
} else { | ||
if (signal.aborted) { | ||
// DOMException not available < v17.0.0 | ||
const err = new Error('The operation was aborted') | ||
err.name = 'AbortError' | ||
throw err | ||
} | ||
} | ||
} | ||
const hasToWellFormed = !!String.prototype.toWellFormed | ||
/** | ||
* @param {string} val | ||
*/ | ||
function toUSVString (val) { | ||
if (hasToWellFormed) { | ||
return `${val}`.toWellFormed() | ||
} else if (nodeUtil.toUSVString) { | ||
return nodeUtil.toUSVString(val) | ||
} | ||
return `${val}` | ||
} | ||
const kEnumerableProperty = Object.create(null) | ||
@@ -411,3 +456,3 @@ kEnumerableProperty.enumerable = true | ||
isReadable, | ||
toUSVString: nodeUtil.toUSVString || ((val) => `${val}`), | ||
toUSVString, | ||
isReadableAborted, | ||
@@ -434,2 +479,3 @@ isBlobLike, | ||
buildURL, | ||
throwIfAborted, | ||
nodeMajor, | ||
@@ -436,0 +482,0 @@ nodeMinor, |
@@ -22,3 +22,3 @@ 'use strict' | ||
this[kDestroyed] = false | ||
this[kOnDestroyed] = [] | ||
this[kOnDestroyed] = null | ||
this[kClosed] = false | ||
@@ -131,2 +131,3 @@ this[kOnClosed] = [] | ||
this[kDestroyed] = true | ||
this[kOnDestroyed] = this[kOnDestroyed] || [] | ||
this[kOnDestroyed].push(callback) | ||
@@ -172,3 +173,3 @@ | ||
if (this[kDestroyed]) { | ||
if (this[kDestroyed] || this[kOnDestroyed]) { | ||
throw new ClientDestroyedError() | ||
@@ -175,0 +176,0 @@ } |
@@ -126,2 +126,3 @@ 'use strict' | ||
length = 0 | ||
let hasUnknownSizeValue = false | ||
@@ -142,3 +143,7 @@ for (const [name, value] of object) { | ||
blobParts.push(chunk, value, rn) | ||
length += chunk.byteLength + value.size + rn.byteLength | ||
if (typeof value.size === 'number') { | ||
length += chunk.byteLength + value.size + rn.byteLength | ||
} else { | ||
hasUnknownSizeValue = true | ||
} | ||
} | ||
@@ -150,2 +155,5 @@ } | ||
length += chunk.byteLength | ||
if (hasUnknownSizeValue) { | ||
length = null | ||
} | ||
@@ -152,0 +160,0 @@ // Set source to object. |
@@ -51,2 +51,3 @@ 'use strict' | ||
// https://fetch.spec.whatwg.org/#request-body-header-name | ||
const requestBodyHeader = [ | ||
@@ -56,3 +57,8 @@ 'content-encoding', | ||
'content-location', | ||
'content-type' | ||
'content-type', | ||
// See https://github.com/nodejs/undici/issues/2021 | ||
// 'Content-Length' is a forbidden header name, which is typically | ||
// removed in the Headers implementation. However, undici doesn't | ||
// filter out headers, so we add it here. | ||
'content-length' | ||
] | ||
@@ -59,0 +65,0 @@ |
const assert = require('assert') | ||
const { atob } = require('buffer') | ||
const { format } = require('url') | ||
const { isValidHTTPToken, isomorphicDecode } = require('./util') | ||
const { isomorphicDecode } = require('./util') | ||
const encoder = new TextEncoder() | ||
// Regex | ||
const HTTP_TOKEN_CODEPOINTS = /^[!#$%&'*+-.^_|~A-z0-9]+$/ | ||
/** | ||
* @see https://mimesniff.spec.whatwg.org/#http-token-code-point | ||
*/ | ||
const HTTP_TOKEN_CODEPOINTS = /^[!#$%&'*+-.^_|~A-Za-z0-9]+$/ | ||
const HTTP_WHITESPACE_REGEX = /(\u000A|\u000D|\u0009|\u0020)/ // eslint-disable-line | ||
// https://mimesniff.spec.whatwg.org/#http-quoted-string-token-code-point | ||
const HTTP_QUOTED_STRING_TOKENS = /^(\u0009|\x{0020}-\x{007E}|\x{0080}-\x{00FF})+$/ // eslint-disable-line | ||
/** | ||
* @see https://mimesniff.spec.whatwg.org/#http-quoted-string-token-code-point | ||
*/ | ||
const HTTP_QUOTED_STRING_TOKENS = /[\u0009|\u0020-\u007E|\u0080-\u00FF]/ // eslint-disable-line | ||
@@ -42,4 +45,2 @@ // https://fetch.spec.whatwg.org/#data-url-processor | ||
// from mimeType. | ||
// Note: This will only remove U+0020 SPACE code | ||
// points, if any. | ||
// Undici implementation note: we need to store the | ||
@@ -50,3 +51,3 @@ // length because if the mimetype has spaces removed, | ||
const mimeTypeLength = mimeType.length | ||
mimeType = mimeType.replace(/^(\u0020)+|(\u0020)+$/g, '') | ||
mimeType = removeASCIIWhitespace(mimeType, true, true) | ||
@@ -123,3 +124,13 @@ // 7. If position is past the end of input, then | ||
function URLSerializer (url, excludeFragment = false) { | ||
return format(url, { fragment: !excludeFragment }) | ||
const href = url.href | ||
if (!excludeFragment) { | ||
return href | ||
} | ||
const hash = href.lastIndexOf('#') | ||
if (hash === -1) { | ||
return href | ||
} | ||
return href.slice(0, hash) | ||
} | ||
@@ -230,3 +241,3 @@ | ||
// from input. | ||
input = input.trim() | ||
input = removeHTTPWhitespace(input, true, true) | ||
@@ -272,3 +283,3 @@ // 2. Let position be a position variable for input, | ||
// 8. Remove any trailing HTTP whitespace from subtype. | ||
subtype = subtype.trimEnd() | ||
subtype = removeHTTPWhitespace(subtype, false, true) | ||
@@ -281,2 +292,5 @@ // 9. If subtype is the empty string or does not solely | ||
const typeLowercase = type.toLowerCase() | ||
const subtypeLowercase = subtype.toLowerCase() | ||
// 10. Let mimeType be a new MIME type record whose type | ||
@@ -287,8 +301,8 @@ // is type, in ASCII lowercase, and subtype is subtype, | ||
const mimeType = { | ||
type: type.toLowerCase(), | ||
subtype: subtype.toLowerCase(), | ||
type: typeLowercase, | ||
subtype: subtypeLowercase, | ||
/** @type {Map<string, string>} */ | ||
parameters: new Map(), | ||
// https://mimesniff.spec.whatwg.org/#mime-type-essence | ||
essence: `${type}/${subtype}` | ||
essence: `${typeLowercase}/${subtypeLowercase}` | ||
} | ||
@@ -371,4 +385,3 @@ | ||
// 2. Remove any trailing HTTP whitespace from parameterValue. | ||
// Note: it says "trailing" whitespace; leading is fine. | ||
parameterValue = parameterValue.trimEnd() | ||
parameterValue = removeHTTPWhitespace(parameterValue, false, true) | ||
@@ -390,3 +403,3 @@ // 3. If parameterValue is the empty string, then continue. | ||
HTTP_TOKEN_CODEPOINTS.test(parameterName) && | ||
!HTTP_QUOTED_STRING_TOKENS.test(parameterValue) && | ||
(parameterValue.length === 0 || HTTP_QUOTED_STRING_TOKENS.test(parameterValue)) && | ||
!mimeType.parameters.has(parameterName) | ||
@@ -525,7 +538,7 @@ ) { | ||
assert(mimeType !== 'failure') | ||
const { type, subtype, parameters } = mimeType | ||
const { parameters, essence } = mimeType | ||
// 1. Let serialization be the concatenation of mimeType’s | ||
// type, U+002F (/), and mimeType’s subtype. | ||
let serialization = `${type}/${subtype}` | ||
let serialization = essence | ||
@@ -545,3 +558,3 @@ // 2. For each name → value of mimeType’s parameters: | ||
// points or value is the empty string, then: | ||
if (!isValidHTTPToken(value)) { | ||
if (!HTTP_TOKEN_CODEPOINTS.test(value)) { | ||
// 1. Precede each occurence of U+0022 (") or | ||
@@ -566,2 +579,55 @@ // U+005C (\) in value with U+005C (\). | ||
/** | ||
* @see https://fetch.spec.whatwg.org/#http-whitespace | ||
* @param {string} char | ||
*/ | ||
function isHTTPWhiteSpace (char) { | ||
return char === '\r' || char === '\n' || char === '\t' || char === ' ' | ||
} | ||
/** | ||
* @see https://fetch.spec.whatwg.org/#http-whitespace | ||
* @param {string} str | ||
*/ | ||
function removeHTTPWhitespace (str, leading = true, trailing = true) { | ||
let lead = 0 | ||
let trail = str.length - 1 | ||
if (leading) { | ||
for (; lead < str.length && isHTTPWhiteSpace(str[lead]); lead++); | ||
} | ||
if (trailing) { | ||
for (; trail > 0 && isHTTPWhiteSpace(str[trail]); trail--); | ||
} | ||
return str.slice(lead, trail + 1) | ||
} | ||
/** | ||
* @see https://infra.spec.whatwg.org/#ascii-whitespace | ||
* @param {string} char | ||
*/ | ||
function isASCIIWhitespace (char) { | ||
return char === '\r' || char === '\n' || char === '\t' || char === '\f' || char === ' ' | ||
} | ||
/** | ||
* @see https://infra.spec.whatwg.org/#strip-leading-and-trailing-ascii-whitespace | ||
*/ | ||
function removeASCIIWhitespace (str, leading = true, trailing = true) { | ||
let lead = 0 | ||
let trail = str.length - 1 | ||
if (leading) { | ||
for (; lead < str.length && isASCIIWhitespace(str[lead]); lead++); | ||
} | ||
if (trailing) { | ||
for (; trail > 0 && isASCIIWhitespace(str[trail]); trail--); | ||
} | ||
return str.slice(lead, trail + 1) | ||
} | ||
module.exports = { | ||
@@ -568,0 +634,0 @@ dataURLProcessor, |
@@ -64,10 +64,3 @@ 'use strict' | ||
// is name from this’s entry list. | ||
const next = [] | ||
for (const entry of this[kState]) { | ||
if (entry.name !== name) { | ||
next.push(entry) | ||
} | ||
} | ||
this[kState] = next | ||
this[kState] = this[kState].filter(entry => entry.name !== name) | ||
} | ||
@@ -74,0 +67,0 @@ |
@@ -6,3 +6,3 @@ // https://github.com/Ethan-Arrowood/undici-fetch | ||
const { kHeadersList } = require('../core/symbols') | ||
const { kGuard, kHeadersCaseInsensitive } = require('./symbols') | ||
const { kGuard } = require('./symbols') | ||
const { kEnumerableProperty } = require('../core/util') | ||
@@ -99,2 +99,3 @@ const { | ||
this[kHeadersSortedMap] = null | ||
this.cookies = null | ||
} | ||
@@ -177,11 +178,12 @@ | ||
get [kHeadersCaseInsensitive] () { | ||
/** @type {string[]} */ | ||
const flatList = [] | ||
get entries () { | ||
const headers = {} | ||
for (const { name, value } of this[kHeadersMap].values()) { | ||
flatList.push(name, value) | ||
if (this[kHeadersMap].size) { | ||
for (const { name, value } of this[kHeadersMap].values()) { | ||
headers[name] = value | ||
} | ||
} | ||
return flatList | ||
return headers | ||
} | ||
@@ -521,2 +523,3 @@ } | ||
set: kEnumerableProperty, | ||
getSetCookie: kEnumerableProperty, | ||
keys: kEnumerableProperty, | ||
@@ -523,0 +526,0 @@ values: kEnumerableProperty, |
@@ -12,3 +12,4 @@ /* globals AbortController */ | ||
sameOrigin, | ||
normalizeMethod | ||
normalizeMethod, | ||
makePolicyContainer | ||
} = require('./util') | ||
@@ -32,3 +33,3 @@ const { | ||
const assert = require('assert') | ||
const { setMaxListeners, getEventListeners, defaultMaxListeners } = require('events') | ||
const { getMaxListeners, setMaxListeners, getEventListeners, defaultMaxListeners } = require('events') | ||
@@ -38,2 +39,3 @@ let TransformStream = globalThis.TransformStream | ||
const kInit = Symbol('init') | ||
const kAbortController = Symbol('abortController') | ||
@@ -57,6 +59,10 @@ const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => { | ||
// TODO | ||
// https://html.spec.whatwg.org/multipage/webappapis.html#environment-settings-object | ||
this[kRealm] = { | ||
settingsObject: { | ||
baseUrl: getGlobalOrigin() | ||
baseUrl: getGlobalOrigin(), | ||
get origin () { | ||
return this.baseUrl?.origin | ||
}, | ||
policyContainer: makePolicyContainer() | ||
} | ||
@@ -130,3 +136,3 @@ } | ||
// 10. If init["window"] exists and is non-null, then throw a TypeError. | ||
if (init.window !== undefined && init.window != null) { | ||
if (init.window != null) { | ||
throw new TypeError(`'window' option '${window}' must be null`) | ||
@@ -136,3 +142,3 @@ } | ||
// 11. If init["window"] exists, then set window to "no-window". | ||
if (init.window !== undefined) { | ||
if ('window' in init) { | ||
window = 'no-window' | ||
@@ -358,13 +364,30 @@ } | ||
} else { | ||
// Keep a strong ref to ac while request object | ||
// is alive. This is needed to prevent AbortController | ||
// from being prematurely garbage collected. | ||
// See, https://github.com/nodejs/undici/issues/1926. | ||
this[kAbortController] = ac | ||
const acRef = new WeakRef(ac) | ||
const abort = function () { | ||
acRef.deref()?.abort(this.reason) | ||
const ac = acRef.deref() | ||
if (ac !== undefined) { | ||
ac.abort(this.reason) | ||
} | ||
} | ||
if (getEventListeners(signal, 'abort').length >= defaultMaxListeners) { | ||
setMaxListeners(100, signal) | ||
} | ||
// Third-party AbortControllers may not work with these. | ||
// See, https://github.com/nodejs/undici/pull/1910#issuecomment-1464495619. | ||
try { | ||
// If the max amount of listeners is equal to the default, increase it | ||
// This is only available in node >= v19.9.0 | ||
if (typeof getMaxListeners === 'function' && getMaxListeners(signal) === defaultMaxListeners) { | ||
setMaxListeners(100, signal) | ||
} else if (getEventListeners(signal, 'abort').length >= defaultMaxListeners) { | ||
setMaxListeners(100, signal) | ||
} | ||
} catch {} | ||
signal.addEventListener('abort', abort, { once: true }) | ||
requestFinalizer.register(this, { signal, abort }) | ||
requestFinalizer.register(ac, { signal, abort }) | ||
} | ||
@@ -429,3 +452,3 @@ } | ||
if ( | ||
((init.body !== undefined && init.body != null) || inputBody != null) && | ||
(init.body != null || inputBody != null) && | ||
(request.method === 'GET' || request.method === 'HEAD') | ||
@@ -440,3 +463,3 @@ ) { | ||
// 36. If init["body"] exists and is non-null, then: | ||
if (init.body !== undefined && init.body != null) { | ||
if (init.body != null) { | ||
// 1. Let Content-Type be null. | ||
@@ -443,0 +466,0 @@ // 2. Set initBody and Content-Type to the result of extracting |
@@ -351,5 +351,3 @@ 'use strict' | ||
? reason | ||
: new Error(reason ? String(reason) : reason, { | ||
cause: isError ? reason : undefined | ||
}), | ||
: new Error(reason ? String(reason) : reason), | ||
aborted: reason && reason.name === 'AbortError' | ||
@@ -473,3 +471,3 @@ }) | ||
if ('headers' in init && init.headers != null) { | ||
fill(response[kState].headersList, init.headers) | ||
fill(response[kHeaders], init.headers) | ||
} | ||
@@ -576,3 +574,4 @@ | ||
filterResponse, | ||
Response | ||
Response, | ||
cloneResponse | ||
} |
@@ -9,4 +9,3 @@ 'use strict' | ||
kGuard: Symbol('guard'), | ||
kRealm: Symbol('realm'), | ||
kHeadersCaseInsensitive: Symbol('headers case insensitive') | ||
kRealm: Symbol('realm') | ||
} |
'use strict' | ||
const { redirectStatus, badPorts, referrerPolicy: referrerPolicyTokens } = require('./constants') | ||
const { getGlobalOrigin } = require('./global') | ||
const { performance } = require('perf_hooks') | ||
@@ -39,5 +40,7 @@ const { isBlobLike, toUSVString, ReadableStreamFrom } = require('../core/util') | ||
// 3. If location is a value, then set location to the result of parsing | ||
// location with response’s URL. | ||
location = location ? new URL(location, responseURL(response)) : null | ||
// 3. If location is a header value, then set location to the result of | ||
// parsing location with response’s URL. | ||
if (location !== null && isValidHeaderValue(location)) { | ||
location = new URL(location, responseURL(response)) | ||
} | ||
@@ -65,3 +68,3 @@ // 4. If location is a URL whose fragment is null, then set location’s | ||
// then return blocked. | ||
if (/^https?:/.test(url.protocol) && badPorts.includes(url.port)) { | ||
if (urlIsHttpHttpsScheme(url) && badPorts.includes(url.port)) { | ||
return 'blocked' | ||
@@ -272,3 +275,3 @@ } | ||
if (serializedOrigin) { | ||
request.headersList.append('Origin', serializedOrigin) | ||
request.headersList.append('origin', serializedOrigin) | ||
} | ||
@@ -288,3 +291,3 @@ | ||
// If request’s origin is a tuple origin, its scheme is "https", and request’s current URL’s scheme is not "https", then set serializedOrigin to `null`. | ||
if (/^https:/.test(request.origin) && !/^https:/.test(requestCurrentURL(request))) { | ||
if (request.origin && urlHasHttpsScheme(request.origin) && !urlHasHttpsScheme(requestCurrentURL(request))) { | ||
serializedOrigin = null | ||
@@ -305,3 +308,3 @@ } | ||
// 2. Append (`Origin`, serializedOrigin) to request’s header list. | ||
request.headersList.append('Origin', serializedOrigin) | ||
request.headersList.append('origin', serializedOrigin) | ||
} | ||
@@ -335,10 +338,13 @@ } | ||
function makePolicyContainer () { | ||
// TODO | ||
return {} | ||
// Note: the fetch spec doesn't make use of embedder policy or CSP list | ||
return { | ||
referrerPolicy: 'strict-origin-when-cross-origin' | ||
} | ||
} | ||
// https://html.spec.whatwg.org/multipage/origin.html#clone-a-policy-container | ||
function clonePolicyContainer () { | ||
// TODO | ||
return {} | ||
function clonePolicyContainer (policyContainer) { | ||
return { | ||
referrerPolicy: policyContainer.referrerPolicy | ||
} | ||
} | ||
@@ -351,100 +357,72 @@ | ||
// Return no-referrer when empty or policy says so | ||
if (policy == null || policy === '' || policy === 'no-referrer') { | ||
return 'no-referrer' | ||
} | ||
// Note: policy cannot (shouldn't) be null or an empty string. | ||
assert(policy) | ||
// 2. Let environment be the request client | ||
const environment = request.client | ||
// 2. Let environment be request’s client. | ||
let referrerSource = null | ||
/** | ||
* 3, Switch on request’s referrer: | ||
"client" | ||
If environment’s global object is a Window object, then | ||
Let document be the associated Document of environment’s global object. | ||
If document’s origin is an opaque origin, return no referrer. | ||
While document is an iframe srcdoc document, | ||
let document be document’s browsing context’s browsing context container’s node document. | ||
Let referrerSource be document’s URL. | ||
Otherwise, let referrerSource be environment’s creation URL. | ||
a URL | ||
Let referrerSource be request’s referrer. | ||
*/ | ||
// 3. Switch on request’s referrer: | ||
if (request.referrer === 'client') { | ||
// Not defined in Node but part of the spec | ||
if (request.client?.globalObject?.constructor?.name === 'Window' ) { // eslint-disable-line | ||
const origin = environment.globalObject.self?.origin ?? environment.globalObject.location?.origin | ||
// Note: node isn't a browser and doesn't implement document/iframes, | ||
// so we bypass this step and replace it with our own. | ||
// If document’s origin is an opaque origin, return no referrer. | ||
if (origin == null || origin === 'null') return 'no-referrer' | ||
const globalOrigin = getGlobalOrigin() | ||
// Let referrerSource be document’s URL. | ||
referrerSource = new URL(environment.globalObject.location.href) | ||
} else { | ||
// 3(a)(II) If environment's global object is not Window, | ||
// Let referrerSource be environments creationURL | ||
if (environment?.globalObject?.location == null) { | ||
return 'no-referrer' | ||
} | ||
if (!globalOrigin || globalOrigin.origin === 'null') { | ||
return 'no-referrer' | ||
} | ||
referrerSource = new URL(environment.globalObject.location.href) | ||
} | ||
// note: we need to clone it as it's mutated | ||
referrerSource = new URL(globalOrigin) | ||
} else if (request.referrer instanceof URL) { | ||
// 3(b) If requests's referrer is a URL instance, then make | ||
// referrerSource be requests's referrer. | ||
// Let referrerSource be request’s referrer. | ||
referrerSource = request.referrer | ||
} else { | ||
// If referrerSource neither client nor instance of URL | ||
// then return "no-referrer". | ||
return 'no-referrer' | ||
} | ||
const urlProtocol = referrerSource.protocol | ||
// 4. Let request’s referrerURL be the result of stripping referrerSource for | ||
// use as a referrer. | ||
let referrerURL = stripURLForReferrer(referrerSource) | ||
// If url's scheme is a local scheme (i.e. one of "about", "data", "javascript", "file") | ||
// then return "no-referrer". | ||
if ( | ||
urlProtocol === 'about:' || urlProtocol === 'data:' || | ||
urlProtocol === 'blob:' | ||
) { | ||
return 'no-referrer' | ||
// 5. Let referrerOrigin be the result of stripping referrerSource for use as | ||
// a referrer, with the origin-only flag set to true. | ||
const referrerOrigin = stripURLForReferrer(referrerSource, true) | ||
// 6. If the result of serializing referrerURL is a string whose length is | ||
// greater than 4096, set referrerURL to referrerOrigin. | ||
if (referrerURL.toString().length > 4096) { | ||
referrerURL = referrerOrigin | ||
} | ||
let temp | ||
let referrerOrigin | ||
// 4. Let requests's referrerURL be the result of stripping referrer | ||
// source for use as referrer (using util function, without origin only) | ||
const referrerUrl = (temp = stripURLForReferrer(referrerSource)).length > 4096 | ||
// 5. Let referrerOrigin be the result of stripping referrer | ||
// source for use as referrer (using util function, with originOnly true) | ||
? (referrerOrigin = stripURLForReferrer(referrerSource, true)) | ||
// 6. If result of seralizing referrerUrl is a string whose length is greater than | ||
// 4096, then set referrerURL to referrerOrigin | ||
: temp | ||
const areSameOrigin = sameOrigin(request, referrerUrl) | ||
const isNonPotentiallyTrustWorthy = isURLPotentiallyTrustworthy(referrerUrl) && | ||
const areSameOrigin = sameOrigin(request, referrerURL) | ||
const isNonPotentiallyTrustWorthy = isURLPotentiallyTrustworthy(referrerURL) && | ||
!isURLPotentiallyTrustworthy(request.url) | ||
// NOTE: How to treat step 7? | ||
// 8. Execute the switch statements corresponding to the value of policy: | ||
switch (policy) { | ||
case 'origin': return referrerOrigin != null ? referrerOrigin : stripURLForReferrer(referrerSource, true) | ||
case 'unsafe-url': return referrerUrl | ||
case 'unsafe-url': return referrerURL | ||
case 'same-origin': | ||
return areSameOrigin ? referrerOrigin : 'no-referrer' | ||
case 'origin-when-cross-origin': | ||
return areSameOrigin ? referrerUrl : referrerOrigin | ||
case 'strict-origin-when-cross-origin': | ||
/** | ||
* 1. If the origin of referrerURL and the origin of request’s current URL are the same, | ||
* then return referrerURL. | ||
* 2. If referrerURL is a potentially trustworthy URL and request’s current URL is not a | ||
* potentially trustworthy URL, then return no referrer. | ||
* 3. Return referrerOrigin | ||
*/ | ||
if (areSameOrigin) return referrerOrigin | ||
// else return isNonPotentiallyTrustWorthy ? 'no-referrer' : referrerOrigin | ||
return areSameOrigin ? referrerURL : referrerOrigin | ||
case 'strict-origin-when-cross-origin': { | ||
const currentURL = requestCurrentURL(request) | ||
// 1. If the origin of referrerURL and the origin of request’s current | ||
// URL are the same, then return referrerURL. | ||
if (sameOrigin(referrerURL, currentURL)) { | ||
return referrerURL | ||
} | ||
// 2. If referrerURL is a potentially trustworthy URL and request’s | ||
// current URL is not a potentially trustworthy URL, then return no | ||
// referrer. | ||
if (isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(currentURL)) { | ||
return 'no-referrer' | ||
} | ||
// 3. Return referrerOrigin. | ||
return referrerOrigin | ||
} | ||
case 'strict-origin': // eslint-disable-line | ||
@@ -468,11 +446,38 @@ /** | ||
} | ||
} | ||
function stripURLForReferrer (url, originOnly = false) { | ||
const urlObject = new URL(url.href) | ||
urlObject.username = '' | ||
urlObject.password = '' | ||
urlObject.hash = '' | ||
/** | ||
* @see https://w3c.github.io/webappsec-referrer-policy/#strip-url | ||
* @param {URL} url | ||
* @param {boolean|undefined} originOnly | ||
*/ | ||
function stripURLForReferrer (url, originOnly) { | ||
// 1. Assert: url is a URL. | ||
assert(url instanceof URL) | ||
return originOnly ? urlObject.origin : urlObject.href | ||
// 2. If url’s scheme is a local scheme, then return no referrer. | ||
if (url.protocol === 'file:' || url.protocol === 'about:' || url.protocol === 'blank:') { | ||
return 'no-referrer' | ||
} | ||
// 3. Set url’s username to the empty string. | ||
url.username = '' | ||
// 4. Set url’s password to the empty string. | ||
url.password = '' | ||
// 5. Set url’s fragment to null. | ||
url.hash = '' | ||
// 6. If the origin-only flag is true, then: | ||
if (originOnly) { | ||
// 1. Set url’s path to « the empty string ». | ||
url.pathname = '' | ||
// 2. Set url’s query to null. | ||
url.search = '' | ||
} | ||
// 7. Return url. | ||
return url | ||
} | ||
@@ -644,3 +649,5 @@ | ||
// 1. If A and B are the same opaque origin, then return true. | ||
// "opaque origin" is an internal value we cannot access, ignore. | ||
if (A.origin === B.origin && A.origin === 'null') { | ||
return true | ||
} | ||
@@ -952,2 +959,37 @@ // 2. If A and B are both tuple origins and their schemes, | ||
/** | ||
* @see https://fetch.spec.whatwg.org/#is-local | ||
* @param {URL} url | ||
*/ | ||
function urlIsLocal (url) { | ||
assert('protocol' in url) // ensure it's a url object | ||
const protocol = url.protocol | ||
return protocol === 'about:' || protocol === 'blob:' || protocol === 'data:' | ||
} | ||
/** | ||
* @param {string|URL} url | ||
*/ | ||
function urlHasHttpsScheme (url) { | ||
if (typeof url === 'string') { | ||
return url.startsWith('https:') | ||
} | ||
return url.protocol === 'https:' | ||
} | ||
/** | ||
* @see https://fetch.spec.whatwg.org/#http-scheme | ||
* @param {URL} url | ||
*/ | ||
function urlIsHttpHttpsScheme (url) { | ||
assert('protocol' in url) // ensure it's a url object | ||
const protocol = url.protocol | ||
return protocol === 'http:' || protocol === 'https:' | ||
} | ||
/** | ||
* Fetch supports node >= 16.8.0, but Object.hasOwn was added in v16.9.0. | ||
@@ -996,3 +1038,7 @@ */ | ||
isomorphicEncode, | ||
isomorphicDecode | ||
isomorphicDecode, | ||
urlIsLocal, | ||
urlHasHttpsScheme, | ||
urlIsHttpHttpsScheme, | ||
readAllBytes | ||
} |
@@ -54,2 +54,9 @@ 'use strict' | ||
webidl.illegalConstructor = function () { | ||
throw webidl.errors.exception({ | ||
header: 'TypeError', | ||
message: 'Illegal constructor' | ||
}) | ||
} | ||
// https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values | ||
@@ -56,0 +63,0 @@ webidl.util.Type = function (V) { |
@@ -5,5 +5,9 @@ 'use strict' | ||
* @see https://encoding.spec.whatwg.org/#concept-encoding-get | ||
* @param {string} label | ||
* @param {string|undefined} label | ||
*/ | ||
function getEncoding (label) { | ||
if (!label) { | ||
return 'failure' | ||
} | ||
// 1. Remove any leading and trailing ASCII whitespace from label. | ||
@@ -10,0 +14,0 @@ // 2. If label is an ASCII case-insensitive match for any of the |
@@ -6,3 +6,3 @@ 'use strict' | ||
const Agent = require('./agent') | ||
const Client = require('./client') | ||
const Pool = require('./pool') | ||
const DispatcherBase = require('./dispatcher-base') | ||
@@ -38,2 +38,6 @@ const { InvalidArgumentError, RequestAbortedError } = require('./core/errors') | ||
function defaultFactory (origin, opts) { | ||
return new Pool(origin, opts) | ||
} | ||
class ProxyAgent extends DispatcherBase { | ||
@@ -56,2 +60,8 @@ constructor (opts) { | ||
const { clientFactory = defaultFactory } = opts | ||
if (typeof clientFactory !== 'function') { | ||
throw new InvalidArgumentError('Proxy opts.clientFactory must be a function.') | ||
} | ||
this[kRequestTls] = opts.requestTls | ||
@@ -75,3 +85,3 @@ this[kProxyTls] = opts.proxyTls | ||
this[kConnectEndpoint] = buildConnector({ ...opts.requestTls }) | ||
this[kClient] = new Client(resolvedUrl, { connect }) | ||
this[kClient] = clientFactory(resolvedUrl, { connect }) | ||
this[kAgent] = new Agent({ | ||
@@ -78,0 +88,0 @@ ...opts, |
@@ -16,9 +16,11 @@ 'use strict' | ||
if (timer.expires && fastNow >= timer.expires) { | ||
timer.expires = 0 | ||
if (timer.state === 0) { | ||
timer.state = fastNow + timer.delay | ||
} else if (timer.state > 0 && fastNow >= timer.state) { | ||
timer.state = -1 | ||
timer.callback(timer.opaque) | ||
} | ||
if (timer.expires === 0) { | ||
timer.active = false | ||
if (timer.state === -1) { | ||
timer.state = -2 | ||
if (idx !== len - 1) { | ||
@@ -57,5 +59,9 @@ fastTimers[idx] = fastTimers.pop() | ||
this.opaque = opaque | ||
this.expires = 0 | ||
this.active = false | ||
// -2 not in timer list | ||
// -1 in timer list but inactive | ||
// 0 in timer list waiting for time | ||
// > 0 in timer list waiting for time to expire | ||
this.state = -2 | ||
this.refresh() | ||
@@ -65,16 +71,14 @@ } | ||
refresh () { | ||
if (!this.active) { | ||
this.active = true | ||
if (this.state === -2) { | ||
fastTimers.push(this) | ||
if (!fastNowTimeout || fastTimers.length === 1) { | ||
refreshTimeout() | ||
fastNow = Date.now() | ||
} | ||
} | ||
this.expires = fastNow + this.delay | ||
this.state = 0 | ||
} | ||
clear () { | ||
this.expires = 0 | ||
this.state = -1 | ||
} | ||
@@ -85,9 +89,13 @@ } | ||
setTimeout (callback, delay, opaque) { | ||
return new Timeout(callback, delay, opaque) | ||
return delay < 1e3 | ||
? setTimeout(callback, delay, opaque) | ||
: new Timeout(callback, delay, opaque) | ||
}, | ||
clearTimeout (timeout) { | ||
if (timeout && timeout.clear) { | ||
if (timeout instanceof Timeout) { | ||
timeout.clear() | ||
} else { | ||
clearTimeout(timeout) | ||
} | ||
} | ||
} |
@@ -8,5 +8,2 @@ 'use strict' | ||
kReadyState, | ||
kResponse, | ||
kExtensions, | ||
kProtocol, | ||
kSentClose, | ||
@@ -18,6 +15,7 @@ kByteParser, | ||
const { CloseEvent } = require('./events') | ||
const { ByteParser } = require('./receiver') | ||
const { makeRequest } = require('../fetch/request') | ||
const { fetching } = require('../fetch/index') | ||
const { getGlobalDispatcher } = require('../..') | ||
const { Headers } = require('../fetch/headers') | ||
const { getGlobalDispatcher } = require('../global') | ||
const { kHeadersList } = require('../core/symbols') | ||
@@ -34,4 +32,6 @@ const channels = {} | ||
* @param {import('./websocket').WebSocket} ws | ||
* @param {(response: any) => void} onEstablish | ||
* @param {Partial<import('../../types/websocket').WebSocketInit>} options | ||
*/ | ||
function establishWebSocketConnection (url, protocols, ws) { | ||
function establishWebSocketConnection (url, protocols, ws, onEstablish, options) { | ||
// 1. Let requestURL be a copy of url, with its scheme set to "http", if url’s | ||
@@ -57,2 +57,9 @@ // scheme is "ws", and to "https" otherwise. | ||
// Note: undici extension, allow setting custom headers. | ||
if (options.headers) { | ||
const headersList = new Headers(options.headers)[kHeadersList] | ||
request.headersList = headersList | ||
} | ||
// 3. Append (`Upgrade`, `websocket`) to request’s header list. | ||
@@ -98,3 +105,3 @@ // 4. Append (`Connection`, `Upgrade`) to request’s header list. | ||
useParallelQueue: true, | ||
dispatcher: getGlobalDispatcher(), | ||
dispatcher: options.dispatcher ?? getGlobalDispatcher(), | ||
processResponse (response) { | ||
@@ -181,12 +188,2 @@ // 1. If response is a network error or its status is not 101, | ||
// processResponse is called when the "response’s header list has been received and initialized." | ||
// once this happens, the connection is open | ||
ws[kResponse] = response | ||
const parser = new ByteParser(ws) | ||
response.socket.ws = ws // TODO: use symbol | ||
ws[kByteParser] = parser | ||
whenConnectionEstablished(ws) | ||
response.socket.on('data', onSocketData) | ||
@@ -196,3 +193,11 @@ response.socket.on('close', onSocketClose) | ||
parser.on('drain', onParserDrain) | ||
if (channels.open.hasSubscribers) { | ||
channels.open.publish({ | ||
address: response.socket.address(), | ||
protocol: secProtocol, | ||
extensions: secExtension | ||
}) | ||
} | ||
onEstablish(response) | ||
} | ||
@@ -205,42 +210,2 @@ }) | ||
/** | ||
* @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol | ||
* @param {import('./websocket').WebSocket} ws | ||
*/ | ||
function whenConnectionEstablished (ws) { | ||
const { [kResponse]: response } = ws | ||
// 1. Change the ready state to OPEN (1). | ||
ws[kReadyState] = states.OPEN | ||
// 2. Change the extensions attribute’s value to the extensions in use, if | ||
// it is not the null value. | ||
// https://datatracker.ietf.org/doc/html/rfc6455#section-9.1 | ||
const extensions = response.headersList.get('sec-websocket-extensions') | ||
if (extensions !== null) { | ||
ws[kExtensions] = extensions | ||
} | ||
// 3. Change the protocol attribute’s value to the subprotocol in use, if | ||
// it is not the null value. | ||
// https://datatracker.ietf.org/doc/html/rfc6455#section-1.9 | ||
const protocol = response.headersList.get('sec-websocket-protocol') | ||
if (protocol !== null) { | ||
ws[kProtocol] = protocol | ||
} | ||
// 4. Fire an event named open at the WebSocket object. | ||
fireEvent('open', ws) | ||
if (channels.open.hasSubscribers) { | ||
channels.open.publish({ | ||
address: response.socket.address(), | ||
protocol, | ||
extensions | ||
}) | ||
} | ||
} | ||
/** | ||
* @param {Buffer} chunk | ||
@@ -254,6 +219,2 @@ */ | ||
function onParserDrain () { | ||
this.ws[kResponse].socket.resume() | ||
} | ||
/** | ||
@@ -260,0 +221,0 @@ * @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol |
@@ -46,3 +46,3 @@ 'use strict' | ||
if (payloadLength === 126) { | ||
new DataView(buffer.buffer).setUint16(2, bodyLength) | ||
buffer.writeUInt16BE(bodyLength, 2) | ||
} else if (payloadLength === 127) { | ||
@@ -49,0 +49,0 @@ // Clear extended payload length |
@@ -8,6 +8,3 @@ 'use strict' | ||
kResponse: Symbol('response'), | ||
kExtensions: Symbol('extensions'), | ||
kProtocol: Symbol('protocol'), | ||
kBinaryType: Symbol('binary type'), | ||
kClosingFrame: Symbol('closing frame'), | ||
kSentClose: Symbol('sent close'), | ||
@@ -14,0 +11,0 @@ kReceivedClose: Symbol('received close'), |
@@ -11,12 +11,13 @@ 'use strict' | ||
kController, | ||
kExtensions, | ||
kProtocol, | ||
kBinaryType, | ||
kResponse, | ||
kSentClose | ||
kSentClose, | ||
kByteParser | ||
} = require('./symbols') | ||
const { isEstablished, isClosing, isValidSubprotocol, failWebsocketConnection } = require('./util') | ||
const { isEstablished, isClosing, isValidSubprotocol, failWebsocketConnection, fireEvent } = require('./util') | ||
const { establishWebSocketConnection } = require('./connection') | ||
const { WebsocketFrameSend } = require('./frame') | ||
const { ByteParser } = require('./receiver') | ||
const { kEnumerableProperty, isBlobLike } = require('../core/util') | ||
const { getGlobalDispatcher } = require('../global') | ||
const { types } = require('util') | ||
@@ -36,2 +37,4 @@ | ||
#bufferedAmount = 0 | ||
#protocol = '' | ||
#extensions = '' | ||
@@ -54,4 +57,6 @@ /** | ||
const options = webidl.converters['DOMString or sequence<DOMString> or WebSocketInit'](protocols) | ||
url = webidl.converters.USVString(url) | ||
protocols = webidl.converters['DOMString or sequence<DOMString>'](protocols) | ||
protocols = options.protocols | ||
@@ -110,3 +115,9 @@ // 1. Let urlRecord be the result of applying the URL parser to url. | ||
// and client. | ||
this[kController] = establishWebSocketConnection(urlRecord, protocols, this) | ||
this[kController] = establishWebSocketConnection( | ||
urlRecord, | ||
protocols, | ||
this, | ||
(response) => this.#onConnectionEstablished(response), | ||
options | ||
) | ||
@@ -119,6 +130,4 @@ // Each WebSocket object has an associated ready state, which is a | ||
// The extensions attribute must initially return the empty string. | ||
this[kExtensions] = '' | ||
// The protocol attribute must initially return the empty string. | ||
this[kProtocol] = '' | ||
@@ -376,3 +385,3 @@ // Each WebSocket object has an associated binary type, which is a | ||
return this[kExtensions] | ||
return this.#extensions | ||
} | ||
@@ -383,3 +392,3 @@ | ||
return this[kProtocol] | ||
return this.#protocol | ||
} | ||
@@ -486,2 +495,43 @@ | ||
} | ||
/** | ||
* @see https://websockets.spec.whatwg.org/#feedback-from-the-protocol | ||
*/ | ||
#onConnectionEstablished (response) { | ||
// processResponse is called when the "response’s header list has been received and initialized." | ||
// once this happens, the connection is open | ||
this[kResponse] = response | ||
const parser = new ByteParser(this) | ||
parser.on('drain', function onParserDrain () { | ||
this.ws[kResponse].socket.resume() | ||
}) | ||
response.socket.ws = this | ||
this[kByteParser] = parser | ||
// 1. Change the ready state to OPEN (1). | ||
this[kReadyState] = states.OPEN | ||
// 2. Change the extensions attribute’s value to the extensions in use, if | ||
// it is not the null value. | ||
// https://datatracker.ietf.org/doc/html/rfc6455#section-9.1 | ||
const extensions = response.headersList.get('sec-websocket-extensions') | ||
if (extensions !== null) { | ||
this.#extensions = extensions | ||
} | ||
// 3. Change the protocol attribute’s value to the subprotocol in use, if | ||
// it is not the null value. | ||
// https://datatracker.ietf.org/doc/html/rfc6455#section-1.9 | ||
const protocol = response.headersList.get('sec-websocket-protocol') | ||
if (protocol !== null) { | ||
this.#protocol = protocol | ||
} | ||
// 4. Fire an event named open at the WebSocket object. | ||
fireEvent('open', this) | ||
} | ||
} | ||
@@ -542,2 +592,32 @@ | ||
// This implements the propsal made in https://github.com/whatwg/websockets/issues/42 | ||
webidl.converters.WebSocketInit = webidl.dictionaryConverter([ | ||
{ | ||
key: 'protocols', | ||
converter: webidl.converters['DOMString or sequence<DOMString>'], | ||
get defaultValue () { | ||
return [] | ||
} | ||
}, | ||
{ | ||
key: 'dispatcher', | ||
converter: (V) => V, | ||
get defaultValue () { | ||
return getGlobalDispatcher() | ||
} | ||
}, | ||
{ | ||
key: 'headers', | ||
converter: webidl.nullableConverter(webidl.converters.HeadersInit) | ||
} | ||
]) | ||
webidl.converters['DOMString or sequence<DOMString> or WebSocketInit'] = function (V) { | ||
if (webidl.util.Type(V) === 'Object' && !(Symbol.iterator in V)) { | ||
return webidl.converters.WebSocketInit(V) | ||
} | ||
return { protocols: webidl.converters['DOMString or sequence<DOMString>'](V) } | ||
} | ||
webidl.converters.WebSocketSendData = function (V) { | ||
@@ -544,0 +624,0 @@ if (webidl.util.Type(V) === 'Object') { |
{ | ||
"name": "undici", | ||
"version": "5.20.0", | ||
"version": "5.22.1", | ||
"description": "An HTTP/1.1 client, written from scratch for Node.js", | ||
@@ -45,14 +45,14 @@ "homepage": "https://undici.nodejs.org", | ||
"build:node": "npx esbuild@0.14.38 index-fetch.js --bundle --platform=node --outfile=undici-fetch.js", | ||
"prebuild:wasm": "docker build -t llhttp_wasm_builder -f build/Dockerfile .", | ||
"prebuild:wasm": "node build/wasm.js --prebuild", | ||
"build:wasm": "node build/wasm.js --docker", | ||
"lint": "standard | snazzy", | ||
"lint:fix": "standard --fix | snazzy", | ||
"test": "npm run test:tap && npm run test:node-fetch && npm run test:fetch && npm run test:cookies && npm run test:wpt && npm run test:websocket && npm run test:jest && tsd", | ||
"test": "npm run test:tap && npm run test:node-fetch && npm run test:fetch && npm run test:cookies && npm run test:wpt && npm run test:websocket && npm run test:jest && npm run test:typescript", | ||
"test:cookies": "node scripts/verifyVersion 16 || tap test/cookie/*.js", | ||
"test:node-fetch": "node scripts/verifyVersion.js 16 || mocha test/node-fetch", | ||
"test:fetch": "node scripts/verifyVersion.js 16 || (npm run build:node && tap test/fetch/*.js && tap test/webidl/*.js)", | ||
"test:node-fetch": "node scripts/verifyVersion.js 16 || mocha --exit test/node-fetch", | ||
"test:fetch": "node scripts/verifyVersion.js 16 || (npm run build:node && tap --expose-gc test/fetch/*.js && tap test/webidl/*.js)", | ||
"test:jest": "node scripts/verifyVersion.js 14 || jest", | ||
"test:tap": "tap test/*.js test/diagnostics-channel/*.js", | ||
"test:tdd": "tap test/*.js test/diagnostics-channel/*.js -w", | ||
"test:typescript": "tsd && tsc test/imports/undici-import.ts", | ||
"test:typescript": "node scripts/verifyVersion.js 14 || tsd && tsc --skipLibCheck test/imports/undici-import.ts", | ||
"test:websocket": "node scripts/verifyVersion.js 18 || tap test/websocket/*.js", | ||
@@ -65,3 +65,3 @@ "test:wpt": "node scripts/verifyVersion 18 || (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 --no-warnings test/wpt/start-websockets.mjs)", | ||
"prebench:run": "node benchmarks/wait.js", | ||
"bench:run": "CONNECTIONS=1 node --experimental-wasm-simd benchmarks/benchmark.js; CONNECTIONS=50 node --experimental-wasm-simd benchmarks/benchmark.js", | ||
"bench:run": "CONNECTIONS=1 node benchmarks/benchmark.js; CONNECTIONS=50 node benchmarks/benchmark.js", | ||
"serve:website": "docsify serve .", | ||
@@ -80,3 +80,3 @@ "prepare": "husky install", | ||
"chai-string": "^1.5.0", | ||
"concurrently": "^7.1.0", | ||
"concurrently": "^8.0.1", | ||
"cronometro": "^1.0.5", | ||
@@ -92,2 +92,3 @@ "delay": "^5.0.0", | ||
"jest": "^29.0.2", | ||
"jsdom": "^21.1.0", | ||
"jsfuzz": "^1.0.15", | ||
@@ -104,9 +105,9 @@ "mocha": "^10.0.0", | ||
"tap": "^16.1.0", | ||
"tsd": "^0.25.0", | ||
"typescript": "^4.9.5", | ||
"wait-on": "^6.0.0", | ||
"tsd": "^0.28.1", | ||
"typescript": "^5.0.2", | ||
"wait-on": "^7.0.1", | ||
"ws": "^8.11.0" | ||
}, | ||
"engines": { | ||
"node": ">=12.18" | ||
"node": ">=14.0" | ||
}, | ||
@@ -120,4 +121,3 @@ "standard": { | ||
"lib/llhttp/utils.js", | ||
"test/wpt/tests", | ||
"test/wpt/runner/resources" | ||
"test/wpt/tests" | ||
] | ||
@@ -124,0 +124,0 @@ }, |
@@ -410,3 +410,3 @@ # undici | ||
### Network address family autoselection. | ||
### Network address family autoselection. | ||
@@ -413,0 +413,0 @@ If you experience problem when connecting to a remote server that is resolved by your DNS servers to a IPv6 (AAAA record) |
@@ -8,6 +8,6 @@ import Pool from './pool' | ||
declare class BalancedPool extends Dispatcher { | ||
constructor(url: string | URL | string[], options?: Pool.Options); | ||
constructor(url: string | string[] | URL | URL[], options?: Pool.Options); | ||
addUpstream(upstream: string): BalancedPool; | ||
removeUpstream(upstream: string): BalancedPool; | ||
addUpstream(upstream: string | URL): BalancedPool; | ||
removeUpstream(upstream: string | URL): BalancedPool; | ||
upstreams: Array<string>; | ||
@@ -14,0 +14,0 @@ |
@@ -7,6 +7,6 @@ import { URL } from 'url' | ||
export default Client | ||
/** A basic HTTP/1.1 client, mapped on top a single TCP/TLS connection. Pipelining is disabled by default. */ | ||
declare class Client extends Dispatcher { | ||
/** | ||
* A basic HTTP/1.1 client, mapped on top a single TCP/TLS connection. Pipelining is disabled by default. | ||
*/ | ||
export class Client extends Dispatcher { | ||
constructor(url: string | URL, options?: Client.Options); | ||
@@ -21,36 +21,58 @@ /** Property to get and set the pipelining factor. */ | ||
declare namespace Client { | ||
export declare namespace Client { | ||
export interface OptionsInterceptors { | ||
Client: readonly DispatchInterceptor[]; | ||
} | ||
export interface Options { | ||
/** TODO */ | ||
interceptors?: OptionsInterceptors; | ||
/** The maximum length of request headers in bytes. Default: `16384` (16KiB). */ | ||
maxHeaderSize?: number; | ||
/** The amount of time the parser will wait to receive the complete HTTP headers (Node 14 and above only). Default: `300e3` milliseconds (300s). */ | ||
headersTimeout?: number; | ||
/** @deprecated unsupported socketTimeout, use headersTimeout & bodyTimeout instead */ | ||
socketTimeout?: never; | ||
/** @deprecated unsupported requestTimeout, use headersTimeout & bodyTimeout instead */ | ||
requestTimeout?: never; | ||
/** TODO */ | ||
connectTimeout?: number; | ||
/** The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Default: `300e3` milliseconds (300s). */ | ||
bodyTimeout?: number; | ||
/** @deprecated unsupported idleTimeout, use keepAliveTimeout instead */ | ||
idleTimeout?: never; | ||
/** @deprecated unsupported keepAlive, use pipelining=0 instead */ | ||
keepAlive?: never; | ||
/** the timeout after which a socket without active requests will time out. Monitors time between activity on a connected socket. This value may be overridden by *keep-alive* hints from the server. Default: `4e3` milliseconds (4s). */ | ||
keepAliveTimeout?: number | null; | ||
keepAliveTimeout?: number; | ||
/** @deprecated unsupported maxKeepAliveTimeout, use keepAliveMaxTimeout instead */ | ||
maxKeepAliveTimeout?: never; | ||
/** the maximum allowed `idleTimeout` when overridden by *keep-alive* hints from the server. Default: `600e3` milliseconds (10min). */ | ||
keepAliveMaxTimeout?: number | null; | ||
keepAliveMaxTimeout?: number; | ||
/** A number subtracted from server *keep-alive* hints when overriding `idleTimeout` to account for timing inaccuracies caused by e.g. transport latency. Default: `1e3` milliseconds (1s). */ | ||
keepAliveTimeoutThreshold?: number | null; | ||
keepAliveTimeoutThreshold?: number; | ||
/** TODO */ | ||
socketPath?: string; | ||
/** The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Default: `1`. */ | ||
pipelining?: number | null; | ||
/** **/ | ||
connect?: buildConnector.BuildOptions | buildConnector.connector | null; | ||
/** The maximum length of request headers in bytes. Default: `16384` (16KiB). */ | ||
maxHeaderSize?: number | null; | ||
/** The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Default: `300e3` milliseconds (300s). */ | ||
bodyTimeout?: number | null; | ||
/** The amount of time the parser will wait to receive the complete HTTP headers (Node 14 and above only). Default: `300e3` milliseconds (300s). */ | ||
headersTimeout?: number | null; | ||
pipelining?: number; | ||
/** @deprecated use the connect option instead */ | ||
tls?: never; | ||
/** If `true`, an error is thrown when the request content-length header doesn't match the length of the request body. Default: `true`. */ | ||
strictContentLength?: boolean; | ||
/** @deprecated use the connect option instead */ | ||
tls?: TlsOptions | null; | ||
/** */ | ||
/** TODO */ | ||
maxCachedSessions?: number; | ||
/** TODO */ | ||
maxRedirections?: number; | ||
/** TODO */ | ||
connect?: buildConnector.BuildOptions | buildConnector.connector; | ||
/** TODO */ | ||
maxRequestsPerClient?: number; | ||
/** TODO */ | ||
localAddress?: string; | ||
/** Max response body size in bytes, -1 is disabled */ | ||
maxResponseSize?: number | null; | ||
maxResponseSize?: number; | ||
/** Enables a family autodetection algorithm that loosely implements section 5 of RFC 8305. */ | ||
autoSelectFamily?: boolean; | ||
/** The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option. */ | ||
autoSelectFamilyAttemptTimeout?: number; | ||
interceptors?: {Client: readonly DispatchInterceptor[] | undefined} | ||
autoSelectFamilyAttemptTimeout?: number; | ||
} | ||
export interface SocketInfo { | ||
@@ -66,4 +88,4 @@ localAddress?: string | ||
} | ||
} | ||
} | ||
export default Client; |
@@ -30,11 +30,5 @@ import { TLSSocket, ConnectionOptions } from 'tls' | ||
export type connector = connectorAsync | connectorSync | ||
interface connectorSync { | ||
(options: buildConnector.Options): Socket | TLSSocket | ||
} | ||
interface connectorAsync { | ||
export interface connector { | ||
(options: buildConnector.Options, callback: buildConnector.Callback): void | ||
} | ||
} |
@@ -145,2 +145,4 @@ import { URL } from 'url' | ||
responseHeader?: 'raw' | null; | ||
/** Default: `64 KiB` */ | ||
highWaterMark?: number; | ||
} | ||
@@ -147,0 +149,0 @@ export interface PipelineOptions extends RequestOptions { |
@@ -7,3 +7,6 @@ import { IncomingHttpHeaders } from "./header"; | ||
declare namespace Errors { | ||
export class UndiciError extends Error { } | ||
export class UndiciError extends Error { | ||
name: string; | ||
code: string; | ||
} | ||
@@ -35,2 +38,8 @@ /** Connect timeout error. */ | ||
export class ResponseStatusCodeError extends UndiciError { | ||
constructor ( | ||
message?: string, | ||
statusCode?: number, | ||
headers?: IncomingHttpHeaders | string[] | null, | ||
body?: null | Record<string, any> | string | ||
); | ||
name: 'ResponseStatusCodeError'; | ||
@@ -37,0 +46,0 @@ code: 'UND_ERR_RESPONSE_STATUS_CODE'; |
import Agent from './agent' | ||
import buildConnector from './connector'; | ||
import Client from './client' | ||
import Dispatcher from './dispatcher' | ||
import { IncomingHttpHeaders } from './header' | ||
import Pool from './pool' | ||
@@ -26,3 +28,4 @@ export default ProxyAgent | ||
proxyTls?: buildConnector.BuildOptions; | ||
clientFactory?(origin: URL, opts: object): Dispatcher; | ||
} | ||
} |
@@ -173,2 +173,4 @@ // These types are not exported, and are only used internally | ||
illegalConstructor (): never | ||
/** | ||
@@ -175,0 +177,0 @@ * @see https://webidl.spec.whatwg.org/#es-to-record |
/// <reference types="node" /> | ||
import type { Blob } from 'buffer' | ||
import type { MessagePort } from 'worker_threads' | ||
import { | ||
@@ -11,2 +13,4 @@ EventTarget, | ||
} from './patch' | ||
import Dispatcher from './dispatcher' | ||
import { HeadersInit } from './fetch' | ||
@@ -69,3 +73,3 @@ export type BinaryType = 'blob' | 'arraybuffer' | ||
prototype: WebSocket | ||
new (url: string | URL, protocols?: string | string[]): WebSocket | ||
new (url: string | URL, protocols?: string | string[] | WebSocketInit): WebSocket | ||
readonly CLOSED: number | ||
@@ -124,1 +128,7 @@ readonly CLOSING: number | ||
} | ||
interface WebSocketInit { | ||
protocols?: string | string[], | ||
dispatcher?: Dispatcher, | ||
headers?: HeadersInit | ||
} |
Sorry, the diff of this file is too big to display
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
1122723
148
19360
35