undici 7.19.2 → 7.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/docs/api/Client.md +1 -0
- package/docs/docs/api/Dispatcher.md +2 -1
- package/docs/docs/api/H2CClient.md +1 -0
- package/docs/docs/api/MockPool.md +2 -1
- package/index-fetch.js +32 -2
- package/index.js +32 -2
- package/lib/api/api-request.js +1 -0
- package/lib/core/symbols.js +1 -0
- package/lib/dispatcher/agent.js +3 -1
- package/lib/dispatcher/client-h1.js +7 -2
- package/lib/dispatcher/client-h2.js +40 -3
- package/lib/dispatcher/client.js +11 -3
- package/lib/dispatcher/pool-base.js +10 -4
- package/lib/mock/mock-utils.js +35 -2
- package/lib/web/fetch/body.js +38 -56
- package/lib/web/fetch/index.js +7 -13
- package/lib/web/fetch/response.js +2 -1
- package/lib/web/webidl/index.js +52 -0
- package/package.json +18 -18
- package/types/client.d.ts +5 -0
- package/types/dispatcher.d.ts +1 -0
- package/types/webidl.d.ts +6 -0
- package/types/websocket.d.ts +2 -0
package/docs/docs/api/Client.md
CHANGED
|
@@ -34,6 +34,7 @@ Returns: `Client`
|
|
|
34
34
|
* **maxConcurrentStreams**: `number` - Default: `100`. Dictates the maximum number of concurrent streams for a single H2 session. It can be overridden by a SETTINGS remote frame.
|
|
35
35
|
* **initialWindowSize**: `number` (optional) - Default: `262144` (256KB). Sets the HTTP/2 stream-level flow-control window size (SETTINGS_INITIAL_WINDOW_SIZE). Must be a positive integer greater than 0. This default is higher than Node.js core's default (65535 bytes) to improve throughput, Node's choice is very conservative for current high-bandwith networks. See [RFC 7540 Section 6.9.2](https://datatracker.ietf.org/doc/html/rfc7540#section-6.9.2) for more details.
|
|
36
36
|
* **connectionWindowSize**: `number` (optional) - Default `524288` (512KB). Sets the HTTP/2 connection-level flow-control window size using `ClientHttp2Session.setLocalWindowSize()`. Must be a positive integer greater than 0. This provides better flow control for the entire connection across multiple streams. See [Node.js HTTP/2 documentation](https://nodejs.org/api/http2.html#clienthttp2sessionsetlocalwindowsize) for more details.
|
|
37
|
+
* **pingInterval**: `number` - Default: `60e3`. The time interval in milliseconds between PING frames sent to the server. Set to `0` to disable PING frames. This is only applicable for HTTP/2 connections. This will emit a `ping` event on the client with the duration of the ping in milliseconds.
|
|
37
38
|
|
|
38
39
|
> **Notes about HTTP/2**
|
|
39
40
|
> - It only works under TLS connections. h2c is not supported.
|
|
@@ -476,6 +476,7 @@ The `RequestOptions.method` property should not be value `'CONNECT'`.
|
|
|
476
476
|
#### Parameter: `ResponseData`
|
|
477
477
|
|
|
478
478
|
* **statusCode** `number`
|
|
479
|
+
* **statusText** `string` - The status message from the response (e.g., "OK", "Not Found").
|
|
479
480
|
* **headers** `Record<string, string | string[]>` - Note that all header keys are lower-cased, e.g. `content-type`.
|
|
480
481
|
* **body** `stream.Readable` which also implements [the body mixin from the Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin).
|
|
481
482
|
* **trailers** `Record<string, string>` - This object starts out
|
|
@@ -517,7 +518,7 @@ await once(server, 'listening')
|
|
|
517
518
|
const client = new Client(`http://localhost:${server.address().port}`)
|
|
518
519
|
|
|
519
520
|
try {
|
|
520
|
-
const { body, headers, statusCode, trailers } = await client.request({
|
|
521
|
+
const { body, headers, statusCode, statusText, trailers } = await client.request({
|
|
521
522
|
path: '/',
|
|
522
523
|
method: 'GET'
|
|
523
524
|
})
|
|
@@ -48,6 +48,7 @@ Returns: `H2CClient`
|
|
|
48
48
|
- **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable.
|
|
49
49
|
- **maxConcurrentStreams**: `number` - Default: `100`. Dictates the maximum number of concurrent streams for a single H2 session. It can be overridden by a SETTINGS remote frame.
|
|
50
50
|
- **pipelining** `number | null` (optional) - Default to `maxConcurrentStreams` - The amount of concurrent requests sent over a single HTTP/2 session in accordance with [RFC-7540](https://httpwg.org/specs/rfc7540.html#StreamsLayer) Stream specification. Streams can be closed up by remote server at any time.
|
|
51
|
+
- **pingInterval**: `number` - Default: `60e3`. The time interval in milliseconds between PING frames sent to the server. Set to `0` to disable PING frames. This is only applicable for HTTP/2 connections.
|
|
51
52
|
- **connect** `ConnectOptions | null` (optional) - Default: `null`.
|
|
52
53
|
- **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body. **Security Warning:** Disabling this option can expose your application to HTTP Request Smuggling attacks, where mismatched content-length headers cause servers and proxies to interpret request boundaries differently. This can lead to cache poisoning, credential hijacking, and bypassing security controls. Only disable this in controlled environments where you fully trust the request source.
|
|
53
54
|
- **autoSelectFamily**: `boolean` (optional) - Default: depends on local Node version, on Node 18.13.0 and above is `false`. Enables a family autodetection algorithm that loosely implements section 5 of [RFC 8305](https://tools.ietf.org/html/rfc8305#section-5). See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details. This option is ignored if not supported by the current Node version.
|
package/index-fetch.js
CHANGED
|
@@ -4,10 +4,40 @@ const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global')
|
|
|
4
4
|
const EnvHttpProxyAgent = require('./lib/dispatcher/env-http-proxy-agent')
|
|
5
5
|
const fetchImpl = require('./lib/web/fetch').fetch
|
|
6
6
|
|
|
7
|
+
// Capture __filename at module load time for stack trace augmentation.
|
|
8
|
+
// This may be undefined when bundled in environments like Node.js internals.
|
|
9
|
+
const currentFilename = typeof __filename !== 'undefined' ? __filename : undefined
|
|
10
|
+
|
|
11
|
+
function appendFetchStackTrace (err, filename) {
|
|
12
|
+
if (!err || typeof err !== 'object') {
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const stack = typeof err.stack === 'string' ? err.stack : ''
|
|
17
|
+
const normalizedFilename = filename.replace(/\\/g, '/')
|
|
18
|
+
|
|
19
|
+
if (stack && (stack.includes(filename) || stack.includes(normalizedFilename))) {
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const capture = {}
|
|
24
|
+
Error.captureStackTrace(capture, appendFetchStackTrace)
|
|
25
|
+
|
|
26
|
+
if (!capture.stack) {
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const captureLines = capture.stack.split('\n').slice(1).join('\n')
|
|
31
|
+
|
|
32
|
+
err.stack = stack ? `${stack}\n${captureLines}` : capture.stack
|
|
33
|
+
}
|
|
34
|
+
|
|
7
35
|
module.exports.fetch = function fetch (init, options = undefined) {
|
|
8
36
|
return fetchImpl(init, options).catch(err => {
|
|
9
|
-
if (
|
|
10
|
-
|
|
37
|
+
if (currentFilename) {
|
|
38
|
+
appendFetchStackTrace(err, currentFilename)
|
|
39
|
+
} else if (err && typeof err === 'object') {
|
|
40
|
+
Error.captureStackTrace(err, module.exports.fetch)
|
|
11
41
|
}
|
|
12
42
|
throw err
|
|
13
43
|
})
|
package/index.js
CHANGED
|
@@ -121,10 +121,40 @@ module.exports.getGlobalDispatcher = getGlobalDispatcher
|
|
|
121
121
|
|
|
122
122
|
const fetchImpl = require('./lib/web/fetch').fetch
|
|
123
123
|
|
|
124
|
+
// Capture __filename at module load time for stack trace augmentation.
|
|
125
|
+
// This may be undefined when bundled in environments like Node.js internals.
|
|
126
|
+
const currentFilename = typeof __filename !== 'undefined' ? __filename : undefined
|
|
127
|
+
|
|
128
|
+
function appendFetchStackTrace (err, filename) {
|
|
129
|
+
if (!err || typeof err !== 'object') {
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const stack = typeof err.stack === 'string' ? err.stack : ''
|
|
134
|
+
const normalizedFilename = filename.replace(/\\/g, '/')
|
|
135
|
+
|
|
136
|
+
if (stack && (stack.includes(filename) || stack.includes(normalizedFilename))) {
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const capture = {}
|
|
141
|
+
Error.captureStackTrace(capture, appendFetchStackTrace)
|
|
142
|
+
|
|
143
|
+
if (!capture.stack) {
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const captureLines = capture.stack.split('\n').slice(1).join('\n')
|
|
148
|
+
|
|
149
|
+
err.stack = stack ? `${stack}\n${captureLines}` : capture.stack
|
|
150
|
+
}
|
|
151
|
+
|
|
124
152
|
module.exports.fetch = function fetch (init, options = undefined) {
|
|
125
153
|
return fetchImpl(init, options).catch(err => {
|
|
126
|
-
if (
|
|
127
|
-
|
|
154
|
+
if (currentFilename) {
|
|
155
|
+
appendFetchStackTrace(err, currentFilename)
|
|
156
|
+
} else if (err && typeof err === 'object') {
|
|
157
|
+
Error.captureStackTrace(err, module.exports.fetch)
|
|
128
158
|
}
|
|
129
159
|
throw err
|
|
130
160
|
})
|
package/lib/api/api-request.js
CHANGED
package/lib/core/symbols.js
CHANGED
|
@@ -67,6 +67,7 @@ module.exports = {
|
|
|
67
67
|
kEnableConnectProtocol: Symbol('http2session connect protocol'),
|
|
68
68
|
kRemoteSettings: Symbol('http2session remote settings'),
|
|
69
69
|
kHTTP2Stream: Symbol('http2session client stream'),
|
|
70
|
+
kPingInterval: Symbol('ping interval'),
|
|
70
71
|
kNoProxyAgent: Symbol('no proxy agent'),
|
|
71
72
|
kHttpProxyAgent: Symbol('http proxy agent'),
|
|
72
73
|
kHttpsProxyAgent: Symbol('https proxy agent')
|
package/lib/dispatcher/agent.js
CHANGED
|
@@ -92,7 +92,9 @@ class Agent extends DispatcherBase {
|
|
|
92
92
|
if (connected) result.count -= 1
|
|
93
93
|
if (result.count <= 0) {
|
|
94
94
|
this[kClients].delete(key)
|
|
95
|
-
result.dispatcher.
|
|
95
|
+
if (!result.dispatcher.destroyed) {
|
|
96
|
+
result.dispatcher.close()
|
|
97
|
+
}
|
|
96
98
|
}
|
|
97
99
|
this[kOrigins].delete(key)
|
|
98
100
|
}
|
|
@@ -735,8 +735,13 @@ class Parser {
|
|
|
735
735
|
}
|
|
736
736
|
}
|
|
737
737
|
|
|
738
|
-
function onParserTimeout (
|
|
739
|
-
const
|
|
738
|
+
function onParserTimeout (parserWeakRef) {
|
|
739
|
+
const parser = parserWeakRef.deref()
|
|
740
|
+
if (!parser) {
|
|
741
|
+
return
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const { socket, timeoutType, client, paused } = parser
|
|
740
745
|
|
|
741
746
|
if (timeoutType === TIMEOUT_HEADERS) {
|
|
742
747
|
if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) {
|
|
@@ -24,6 +24,7 @@ const {
|
|
|
24
24
|
kStrictContentLength,
|
|
25
25
|
kOnError,
|
|
26
26
|
kMaxConcurrentStreams,
|
|
27
|
+
kPingInterval,
|
|
27
28
|
kHTTP2Session,
|
|
28
29
|
kHTTP2InitialWindowSize,
|
|
29
30
|
kHTTP2ConnectionWindowSize,
|
|
@@ -34,7 +35,8 @@ const {
|
|
|
34
35
|
kBodyTimeout,
|
|
35
36
|
kEnableConnectProtocol,
|
|
36
37
|
kRemoteSettings,
|
|
37
|
-
kHTTP2Stream
|
|
38
|
+
kHTTP2Stream,
|
|
39
|
+
kHTTP2SessionState
|
|
38
40
|
} = require('../core/symbols.js')
|
|
39
41
|
const { channels } = require('../core/diagnostics.js')
|
|
40
42
|
|
|
@@ -102,10 +104,15 @@ function connectH2 (client, socket) {
|
|
|
102
104
|
}
|
|
103
105
|
})
|
|
104
106
|
|
|
107
|
+
client[kSocket] = socket
|
|
105
108
|
session[kOpenStreams] = 0
|
|
106
109
|
session[kClient] = client
|
|
107
110
|
session[kSocket] = socket
|
|
108
|
-
session[
|
|
111
|
+
session[kHTTP2SessionState] = {
|
|
112
|
+
ping: {
|
|
113
|
+
interval: client[kPingInterval] === 0 ? null : setInterval(onHttp2SendPing, client[kPingInterval], session).unref()
|
|
114
|
+
}
|
|
115
|
+
}
|
|
109
116
|
// We set it to true by default in a best-effort; however once connected to an H2 server
|
|
110
117
|
// we will check if extended CONNECT protocol is supported or not
|
|
111
118
|
// and set this value accordingly.
|
|
@@ -253,6 +260,31 @@ function onHttp2RemoteSettings (settings) {
|
|
|
253
260
|
this[kClient][kResume]()
|
|
254
261
|
}
|
|
255
262
|
|
|
263
|
+
function onHttp2SendPing (session) {
|
|
264
|
+
const state = session[kHTTP2SessionState]
|
|
265
|
+
if ((session.closed || session.destroyed) && state.ping.interval != null) {
|
|
266
|
+
clearInterval(state.ping.interval)
|
|
267
|
+
state.ping.interval = null
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// If no ping sent, do nothing
|
|
272
|
+
session.ping(onPing.bind(session))
|
|
273
|
+
|
|
274
|
+
function onPing (err, duration) {
|
|
275
|
+
const client = this[kClient]
|
|
276
|
+
const socket = this[kClient]
|
|
277
|
+
|
|
278
|
+
if (err != null) {
|
|
279
|
+
const error = new InformationalError(`HTTP/2: "PING" errored - type ${err.message}`)
|
|
280
|
+
socket[kError] = error
|
|
281
|
+
client[kOnError](error)
|
|
282
|
+
} else {
|
|
283
|
+
client.emit('ping', duration)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
256
288
|
function onHttp2SessionError (err) {
|
|
257
289
|
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
|
|
258
290
|
|
|
@@ -316,7 +348,7 @@ function onHttp2SessionGoAway (errorCode) {
|
|
|
316
348
|
}
|
|
317
349
|
|
|
318
350
|
function onHttp2SessionClose () {
|
|
319
|
-
const { [kClient]: client } = this
|
|
351
|
+
const { [kClient]: client, [kHTTP2SessionState]: state } = this
|
|
320
352
|
const { [kSocket]: socket } = client
|
|
321
353
|
|
|
322
354
|
const err = this[kSocket][kError] || this[kError] || new SocketError('closed', util.getSocketInfo(socket))
|
|
@@ -324,6 +356,11 @@ function onHttp2SessionClose () {
|
|
|
324
356
|
client[kSocket] = null
|
|
325
357
|
client[kHTTPContext] = null
|
|
326
358
|
|
|
359
|
+
if (state.ping.interval != null) {
|
|
360
|
+
clearInterval(state.ping.interval)
|
|
361
|
+
state.ping.interval = null
|
|
362
|
+
}
|
|
363
|
+
|
|
327
364
|
if (client.destroyed) {
|
|
328
365
|
assert(client[kPending] === 0)
|
|
329
366
|
|
package/lib/dispatcher/client.js
CHANGED
|
@@ -54,7 +54,8 @@ const {
|
|
|
54
54
|
kMaxConcurrentStreams,
|
|
55
55
|
kHTTP2InitialWindowSize,
|
|
56
56
|
kHTTP2ConnectionWindowSize,
|
|
57
|
-
kResume
|
|
57
|
+
kResume,
|
|
58
|
+
kPingInterval
|
|
58
59
|
} = require('../core/symbols.js')
|
|
59
60
|
const connectH1 = require('./client-h1.js')
|
|
60
61
|
const connectH2 = require('./client-h2.js')
|
|
@@ -112,7 +113,8 @@ class Client extends DispatcherBase {
|
|
|
112
113
|
allowH2,
|
|
113
114
|
useH2c,
|
|
114
115
|
initialWindowSize,
|
|
115
|
-
connectionWindowSize
|
|
116
|
+
connectionWindowSize,
|
|
117
|
+
pingInterval
|
|
116
118
|
} = {}) {
|
|
117
119
|
if (keepAlive !== undefined) {
|
|
118
120
|
throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead')
|
|
@@ -216,6 +218,10 @@ class Client extends DispatcherBase {
|
|
|
216
218
|
throw new InvalidArgumentError('connectionWindowSize must be a positive integer, greater than 0')
|
|
217
219
|
}
|
|
218
220
|
|
|
221
|
+
if (pingInterval != null && (typeof pingInterval !== 'number' || !Number.isInteger(pingInterval) || pingInterval < 0)) {
|
|
222
|
+
throw new InvalidArgumentError('pingInterval must be a positive integer, greater or equal to 0')
|
|
223
|
+
}
|
|
224
|
+
|
|
219
225
|
super()
|
|
220
226
|
|
|
221
227
|
if (typeof connect !== 'function') {
|
|
@@ -250,6 +256,8 @@ class Client extends DispatcherBase {
|
|
|
250
256
|
this[kMaxRequests] = maxRequestsPerClient
|
|
251
257
|
this[kClosedResolve] = null
|
|
252
258
|
this[kMaxResponseSize] = maxResponseSize > -1 ? maxResponseSize : -1
|
|
259
|
+
this[kHTTPContext] = null
|
|
260
|
+
// h2
|
|
253
261
|
this[kMaxConcurrentStreams] = maxConcurrentStreams != null ? maxConcurrentStreams : 100 // Max peerConcurrentStreams for a Node h2 server
|
|
254
262
|
// HTTP/2 window sizes are set to higher defaults than Node.js core for better performance:
|
|
255
263
|
// - initialWindowSize: 262144 (256KB) vs Node.js default 65535 (64KB - 1)
|
|
@@ -259,7 +267,7 @@ class Client extends DispatcherBase {
|
|
|
259
267
|
// Provides better flow control for the entire connection across multiple streams.
|
|
260
268
|
this[kHTTP2InitialWindowSize] = initialWindowSize != null ? initialWindowSize : 262144
|
|
261
269
|
this[kHTTP2ConnectionWindowSize] = connectionWindowSize != null ? connectionWindowSize : 524288
|
|
262
|
-
this[
|
|
270
|
+
this[kPingInterval] = pingInterval != null ? pingInterval : 60e3 // Default ping interval for h2 - 1 minute
|
|
263
271
|
|
|
264
272
|
// kQueue is built up of 3 sections separated by
|
|
265
273
|
// the kRunningIdx and kPendingIdx indices.
|
|
@@ -48,9 +48,12 @@ class PoolBase extends DispatcherBase {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
if (this[kClosedResolve] && queue.isEmpty()) {
|
|
51
|
-
const closeAll =
|
|
51
|
+
const closeAll = []
|
|
52
52
|
for (let i = 0; i < this[kClients].length; i++) {
|
|
53
|
-
|
|
53
|
+
const client = this[kClients][i]
|
|
54
|
+
if (!client.destroyed) {
|
|
55
|
+
closeAll.push(client.close())
|
|
56
|
+
}
|
|
54
57
|
}
|
|
55
58
|
return Promise.all(closeAll)
|
|
56
59
|
.then(this[kClosedResolve])
|
|
@@ -119,9 +122,12 @@ class PoolBase extends DispatcherBase {
|
|
|
119
122
|
|
|
120
123
|
[kClose] () {
|
|
121
124
|
if (this[kQueue].isEmpty()) {
|
|
122
|
-
const closeAll =
|
|
125
|
+
const closeAll = []
|
|
123
126
|
for (let i = 0; i < this[kClients].length; i++) {
|
|
124
|
-
|
|
127
|
+
const client = this[kClients][i]
|
|
128
|
+
if (!client.destroyed) {
|
|
129
|
+
closeAll.push(client.close())
|
|
130
|
+
}
|
|
125
131
|
}
|
|
126
132
|
return Promise.all(closeAll)
|
|
127
133
|
} else {
|
package/lib/mock/mock-utils.js
CHANGED
|
@@ -312,9 +312,33 @@ function mockDispatch (opts, handler) {
|
|
|
312
312
|
return true
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
// Track whether the request has been aborted
|
|
316
|
+
let aborted = false
|
|
317
|
+
let timer = null
|
|
318
|
+
|
|
319
|
+
function abort (err) {
|
|
320
|
+
if (aborted) {
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
aborted = true
|
|
324
|
+
|
|
325
|
+
// Clear the pending delayed response if any
|
|
326
|
+
if (timer !== null) {
|
|
327
|
+
clearTimeout(timer)
|
|
328
|
+
timer = null
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Notify the handler of the abort
|
|
332
|
+
handler.onError(err)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Call onConnect to allow the handler to register the abort callback
|
|
336
|
+
handler.onConnect?.(abort, null)
|
|
337
|
+
|
|
315
338
|
// Handle the request with a delay if necessary
|
|
316
339
|
if (typeof delay === 'number' && delay > 0) {
|
|
317
|
-
setTimeout(() => {
|
|
340
|
+
timer = setTimeout(() => {
|
|
341
|
+
timer = null
|
|
318
342
|
handleReply(this[kDispatches])
|
|
319
343
|
}, delay)
|
|
320
344
|
} else {
|
|
@@ -322,6 +346,11 @@ function mockDispatch (opts, handler) {
|
|
|
322
346
|
}
|
|
323
347
|
|
|
324
348
|
function handleReply (mockDispatches, _data = data) {
|
|
349
|
+
// Don't send response if the request was aborted
|
|
350
|
+
if (aborted) {
|
|
351
|
+
return
|
|
352
|
+
}
|
|
353
|
+
|
|
325
354
|
// fetch's HeadersList is a 1D string array
|
|
326
355
|
const optsHeaders = Array.isArray(opts.headers)
|
|
327
356
|
? buildHeadersFromArray(opts.headers)
|
|
@@ -340,11 +369,15 @@ function mockDispatch (opts, handler) {
|
|
|
340
369
|
return body.then((newData) => handleReply(mockDispatches, newData))
|
|
341
370
|
}
|
|
342
371
|
|
|
372
|
+
// Check again if aborted after async body resolution
|
|
373
|
+
if (aborted) {
|
|
374
|
+
return
|
|
375
|
+
}
|
|
376
|
+
|
|
343
377
|
const responseData = getResponseData(body)
|
|
344
378
|
const responseHeaders = generateKeyValues(headers)
|
|
345
379
|
const responseTrailers = generateKeyValues(trailers)
|
|
346
380
|
|
|
347
|
-
handler.onConnect?.(err => handler.onError(err), null)
|
|
348
381
|
handler.onHeaders?.(statusCode, responseHeaders, resume, getStatusText(statusCode))
|
|
349
382
|
handler.onData?.(Buffer.from(responseData))
|
|
350
383
|
handler.onComplete?.(responseTrailers)
|
package/lib/web/fetch/body.js
CHANGED
|
@@ -11,7 +11,7 @@ const { FormData, setFormDataState } = require('./formdata')
|
|
|
11
11
|
const { webidl } = require('../webidl')
|
|
12
12
|
const assert = require('node:assert')
|
|
13
13
|
const { isErrored, isDisturbed } = require('node:stream')
|
|
14
|
-
const {
|
|
14
|
+
const { isUint8Array } = require('node:util/types')
|
|
15
15
|
const { serializeAMimeType } = require('./data-url')
|
|
16
16
|
const { multipartFormDataParser } = require('./formdata-parser')
|
|
17
17
|
const { createDeferredPromise } = require('../../util/promise')
|
|
@@ -45,6 +45,7 @@ const streamRegistry = new FinalizationRegistry((weakRef) => {
|
|
|
45
45
|
function extractBody (object, keepalive = false) {
|
|
46
46
|
// 1. Let stream be null.
|
|
47
47
|
let stream = null
|
|
48
|
+
let controller = null
|
|
48
49
|
|
|
49
50
|
// 2. If object is a ReadableStream object, then set stream to object.
|
|
50
51
|
if (webidl.is.ReadableStream(object)) {
|
|
@@ -57,16 +58,11 @@ function extractBody (object, keepalive = false) {
|
|
|
57
58
|
// 4. Otherwise, set stream to a new ReadableStream object, and set
|
|
58
59
|
// up stream with byte reading support.
|
|
59
60
|
stream = new ReadableStream({
|
|
60
|
-
pull (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (buffer.byteLength) {
|
|
64
|
-
controller.enqueue(buffer)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
queueMicrotask(() => readableStreamClose(controller))
|
|
61
|
+
pull () {},
|
|
62
|
+
start (c) {
|
|
63
|
+
controller = c
|
|
68
64
|
},
|
|
69
|
-
|
|
65
|
+
cancel () {},
|
|
70
66
|
type: 'bytes'
|
|
71
67
|
})
|
|
72
68
|
}
|
|
@@ -108,9 +104,8 @@ function extractBody (object, keepalive = false) {
|
|
|
108
104
|
// Set type to `application/x-www-form-urlencoded;charset=UTF-8`.
|
|
109
105
|
type = 'application/x-www-form-urlencoded;charset=UTF-8'
|
|
110
106
|
} else if (webidl.is.BufferSource(object)) {
|
|
111
|
-
source
|
|
112
|
-
|
|
113
|
-
: new Uint8Array(object.buffer.slice(object.byteOffset, object.byteOffset + object.byteLength))
|
|
107
|
+
// Set source to a copy of the bytes held by object.
|
|
108
|
+
source = webidl.util.getCopyOfBytesHeldByBufferSource(object)
|
|
114
109
|
} else if (webidl.is.FormData(object)) {
|
|
115
110
|
const boundary = `----formdata-undici-0${`${random(1e11)}`.padStart(11, '0')}`
|
|
116
111
|
const prefix = `--${boundary}\r\nContent-Disposition: form-data`
|
|
@@ -213,45 +208,36 @@ function extractBody (object, keepalive = false) {
|
|
|
213
208
|
|
|
214
209
|
// 11. If source is a byte sequence, then set action to a
|
|
215
210
|
// step that returns source and length to source’s length.
|
|
216
|
-
if (typeof source === 'string' ||
|
|
217
|
-
|
|
211
|
+
if (typeof source === 'string' || isUint8Array(source)) {
|
|
212
|
+
action = () => {
|
|
213
|
+
length = typeof source === 'string' ? Buffer.byteLength(source) : source.length
|
|
214
|
+
return source
|
|
215
|
+
}
|
|
218
216
|
}
|
|
219
217
|
|
|
220
|
-
// 12. If action is non-null, then run these steps in
|
|
218
|
+
// 12. If action is non-null, then run these steps in parallel:
|
|
221
219
|
if (action != null) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
controller.byobRequest?.respond(0)
|
|
235
|
-
})
|
|
236
|
-
} else {
|
|
237
|
-
// Whenever one or more bytes are available and stream is not errored,
|
|
238
|
-
// enqueue a Uint8Array wrapping an ArrayBuffer containing the available
|
|
239
|
-
// bytes into stream.
|
|
240
|
-
if (!isErrored(stream)) {
|
|
241
|
-
const buffer = new Uint8Array(value)
|
|
242
|
-
if (buffer.byteLength) {
|
|
243
|
-
controller.enqueue(buffer)
|
|
244
|
-
}
|
|
245
|
-
}
|
|
220
|
+
;(async () => {
|
|
221
|
+
// 1. Run action.
|
|
222
|
+
const result = action()
|
|
223
|
+
|
|
224
|
+
// 2. Whenever one or more bytes are available and stream is not errored,
|
|
225
|
+
// enqueue the result of creating a Uint8Array from the available bytes into stream.
|
|
226
|
+
const iterator = result?.[Symbol.asyncIterator]?.()
|
|
227
|
+
if (iterator) {
|
|
228
|
+
for await (const bytes of iterator) {
|
|
229
|
+
if (isErrored(stream)) break
|
|
230
|
+
if (bytes.length) {
|
|
231
|
+
controller.enqueue(new Uint8Array(bytes))
|
|
246
232
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
})
|
|
233
|
+
}
|
|
234
|
+
} else if (result?.length && !isErrored(stream)) {
|
|
235
|
+
controller.enqueue(typeof result === 'string' ? textEncoder.encode(result) : new Uint8Array(result))
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 3. When running action is done, close stream.
|
|
239
|
+
queueMicrotask(() => readableStreamClose(controller))
|
|
240
|
+
})()
|
|
255
241
|
}
|
|
256
242
|
|
|
257
243
|
// 13. Let body be a body whose stream is stream, source is source,
|
|
@@ -436,18 +422,14 @@ function consumeBody (object, convertBytesToJSValue, instance, getInternalState)
|
|
|
436
422
|
return Promise.reject(e)
|
|
437
423
|
}
|
|
438
424
|
|
|
439
|
-
|
|
425
|
+
object = getInternalState(object)
|
|
440
426
|
|
|
441
427
|
// 1. If object is unusable, then return a promise rejected
|
|
442
428
|
// with a TypeError.
|
|
443
|
-
if (bodyUnusable(
|
|
429
|
+
if (bodyUnusable(object)) {
|
|
444
430
|
return Promise.reject(new TypeError('Body is unusable: Body has already been read'))
|
|
445
431
|
}
|
|
446
432
|
|
|
447
|
-
if (state.aborted) {
|
|
448
|
-
return Promise.reject(new DOMException('The operation was aborted.', 'AbortError'))
|
|
449
|
-
}
|
|
450
|
-
|
|
451
433
|
// 2. Let promise be a new promise.
|
|
452
434
|
const promise = createDeferredPromise()
|
|
453
435
|
|
|
@@ -468,14 +450,14 @@ function consumeBody (object, convertBytesToJSValue, instance, getInternalState)
|
|
|
468
450
|
|
|
469
451
|
// 5. If object’s body is null, then run successSteps with an
|
|
470
452
|
// empty byte sequence.
|
|
471
|
-
if (
|
|
453
|
+
if (object.body == null) {
|
|
472
454
|
successSteps(Buffer.allocUnsafe(0))
|
|
473
455
|
return promise.promise
|
|
474
456
|
}
|
|
475
457
|
|
|
476
458
|
// 6. Otherwise, fully read object’s body given successSteps,
|
|
477
459
|
// errorSteps, and object’s relevant global object.
|
|
478
|
-
fullyReadBody(
|
|
460
|
+
fullyReadBody(object.body, successSteps, errorSteps)
|
|
479
461
|
|
|
480
462
|
// 7. Return promise.
|
|
481
463
|
return promise.promise
|
package/lib/web/fetch/index.js
CHANGED
|
@@ -157,7 +157,7 @@ function fetch (input, init = undefined) {
|
|
|
157
157
|
if (requestObject.signal.aborted) {
|
|
158
158
|
// 1. Abort the fetch() call with p, request, null, and
|
|
159
159
|
// requestObject’s signal’s abort reason.
|
|
160
|
-
abortFetch(p, request, null, requestObject.signal.reason)
|
|
160
|
+
abortFetch(p, request, null, requestObject.signal.reason, null)
|
|
161
161
|
|
|
162
162
|
// 2. Return p.
|
|
163
163
|
return p.promise
|
|
@@ -200,7 +200,7 @@ function fetch (input, init = undefined) {
|
|
|
200
200
|
|
|
201
201
|
// 4. Abort the fetch() call with p, request, responseObject,
|
|
202
202
|
// and requestObject’s signal’s abort reason.
|
|
203
|
-
abortFetch(p, request, realResponse, requestObject.signal.reason)
|
|
203
|
+
abortFetch(p, request, realResponse, requestObject.signal.reason, controller.controller)
|
|
204
204
|
}
|
|
205
205
|
)
|
|
206
206
|
|
|
@@ -227,7 +227,7 @@ function fetch (input, init = undefined) {
|
|
|
227
227
|
// 2. Abort the fetch() call with p, request, responseObject, and
|
|
228
228
|
// deserializedError.
|
|
229
229
|
|
|
230
|
-
abortFetch(p, request, responseObject, controller.serializedAbortReason)
|
|
230
|
+
abortFetch(p, request, responseObject, controller.serializedAbortReason, controller.controller)
|
|
231
231
|
return
|
|
232
232
|
}
|
|
233
233
|
|
|
@@ -327,7 +327,7 @@ function finalizeAndReportTiming (response, initiatorType = 'other') {
|
|
|
327
327
|
const markResourceTiming = performance.markResourceTiming
|
|
328
328
|
|
|
329
329
|
// https://fetch.spec.whatwg.org/#abort-fetch
|
|
330
|
-
function abortFetch (p, request, responseObject, error) {
|
|
330
|
+
function abortFetch (p, request, responseObject, error, controller /* undici-specific */) {
|
|
331
331
|
// 1. Reject promise with error.
|
|
332
332
|
if (p) {
|
|
333
333
|
// We might have already resolved the promise at this stage
|
|
@@ -357,13 +357,7 @@ function abortFetch (p, request, responseObject, error) {
|
|
|
357
357
|
// 5. If response’s body is not null and is readable, then error response’s
|
|
358
358
|
// body with error.
|
|
359
359
|
if (response.body?.stream != null && isReadable(response.body.stream)) {
|
|
360
|
-
|
|
361
|
-
if (err.code === 'ERR_INVALID_STATE') {
|
|
362
|
-
// Node bug?
|
|
363
|
-
return
|
|
364
|
-
}
|
|
365
|
-
throw err
|
|
366
|
-
})
|
|
360
|
+
controller.error(error)
|
|
367
361
|
}
|
|
368
362
|
}
|
|
369
363
|
|
|
@@ -1321,8 +1315,8 @@ function httpRedirectFetch (fetchParams, response) {
|
|
|
1321
1315
|
request.headersList.delete('host', true)
|
|
1322
1316
|
}
|
|
1323
1317
|
|
|
1324
|
-
// 14. If request
|
|
1325
|
-
// value of safely extracting request
|
|
1318
|
+
// 14. If request's body is non-null, then set request's body to the first return
|
|
1319
|
+
// value of safely extracting request's body's source.
|
|
1326
1320
|
if (request.body != null) {
|
|
1327
1321
|
assert(request.body.source != null)
|
|
1328
1322
|
request.body = safelyExtractBody(request.body.source)[0]
|
|
@@ -242,7 +242,8 @@ class Response {
|
|
|
242
242
|
const clonedResponse = cloneResponse(this.#state)
|
|
243
243
|
|
|
244
244
|
// Note: To re-register because of a new stream.
|
|
245
|
-
|
|
245
|
+
// Don't set finalizers other than for fetch responses.
|
|
246
|
+
if (this.#state.urlList.length !== 0 && this.#state.body?.stream) {
|
|
246
247
|
streamRegistry.register(this, new WeakRef(this.#state.body.stream))
|
|
247
248
|
}
|
|
248
249
|
|
package/lib/web/webidl/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const assert = require('node:assert')
|
|
3
4
|
const { types, inspect } = require('node:util')
|
|
4
5
|
const { runtimeFeatures } = require('../../util/runtime-features')
|
|
5
6
|
|
|
@@ -542,6 +543,57 @@ webidl.is.BufferSource = function (V) {
|
|
|
542
543
|
)
|
|
543
544
|
}
|
|
544
545
|
|
|
546
|
+
// https://webidl.spec.whatwg.org/#dfn-get-buffer-source-copy
|
|
547
|
+
webidl.util.getCopyOfBytesHeldByBufferSource = function (bufferSource) {
|
|
548
|
+
// 1. Let jsBufferSource be the result of converting bufferSource to a JavaScript value.
|
|
549
|
+
const jsBufferSource = bufferSource
|
|
550
|
+
|
|
551
|
+
// 2. Let jsArrayBuffer be jsBufferSource.
|
|
552
|
+
let jsArrayBuffer = jsBufferSource
|
|
553
|
+
|
|
554
|
+
// 3. Let offset be 0.
|
|
555
|
+
let offset = 0
|
|
556
|
+
|
|
557
|
+
// 4. Let length be 0.
|
|
558
|
+
let length = 0
|
|
559
|
+
|
|
560
|
+
// 5. If jsBufferSource has a [[ViewedArrayBuffer]] internal slot, then:
|
|
561
|
+
if (types.isTypedArray(jsBufferSource) || types.isDataView(jsBufferSource)) {
|
|
562
|
+
// 5.1. Set jsArrayBuffer to jsBufferSource.[[ViewedArrayBuffer]].
|
|
563
|
+
jsArrayBuffer = jsBufferSource.buffer
|
|
564
|
+
|
|
565
|
+
// 5.2. Set offset to jsBufferSource.[[ByteOffset]].
|
|
566
|
+
offset = jsBufferSource.byteOffset
|
|
567
|
+
|
|
568
|
+
// 5.3. Set length to jsBufferSource.[[ByteLength]].
|
|
569
|
+
length = jsBufferSource.byteLength
|
|
570
|
+
} else {
|
|
571
|
+
// 6. Otherwise:
|
|
572
|
+
|
|
573
|
+
// 6.1. Assert: jsBufferSource is an ArrayBuffer or SharedArrayBuffer object.
|
|
574
|
+
assert(types.isAnyArrayBuffer(jsBufferSource))
|
|
575
|
+
|
|
576
|
+
// 6.2. Set length to jsBufferSource.[[ArrayBufferByteLength]].
|
|
577
|
+
length = jsBufferSource.byteLength
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// 7. If IsDetachedBuffer(jsArrayBuffer) is true, then return the empty byte sequence.
|
|
581
|
+
if (jsArrayBuffer.detached) {
|
|
582
|
+
return new Uint8Array(0)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// 8. Let bytes be a new byte sequence of length equal to length.
|
|
586
|
+
const bytes = new Uint8Array(length)
|
|
587
|
+
|
|
588
|
+
// 9. For i in the range offset to offset + length − 1, inclusive,
|
|
589
|
+
// set bytes[i − offset] to GetValueFromBuffer(jsArrayBuffer, i, Uint8, true, Unordered).
|
|
590
|
+
const view = new Uint8Array(jsArrayBuffer, offset, length)
|
|
591
|
+
bytes.set(view)
|
|
592
|
+
|
|
593
|
+
// 10. Return bytes.
|
|
594
|
+
return bytes
|
|
595
|
+
}
|
|
596
|
+
|
|
545
597
|
// https://webidl.spec.whatwg.org/#es-DOMString
|
|
546
598
|
webidl.converters.DOMString = function (V, prefix, argument, flags) {
|
|
547
599
|
// 1. If V is null and the conversion is to an IDL type
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "undici",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.21.0",
|
|
4
4
|
"description": "An HTTP/1.1 client, written from scratch for Node.js",
|
|
5
5
|
"homepage": "https://undici.nodejs.org",
|
|
6
6
|
"bugs": {
|
|
@@ -71,29 +71,29 @@
|
|
|
71
71
|
"test:javascript": "npm run test:javascript:no-jest && npm run test:jest",
|
|
72
72
|
"test:javascript:no-jest": "npm run generate-pem && npm run test:unit && npm run test:fetch && npm run test:node-fetch && npm run test:infra && npm run test:cache && npm run test:cache-interceptor && npm run test:interceptors && npm run test:cookies && npm run test:eventsource && npm run test:subresource-integrity && npm run test:wpt && npm run test:websocket && npm run test:node-test && npm run test:cache-tests",
|
|
73
73
|
"test:javascript:without-intl": "npm run test:javascript:no-jest",
|
|
74
|
-
"test:busboy": "borp -p \"test/busboy/*.js\"",
|
|
75
|
-
"test:cache": "borp -p \"test/cache/*.js\"",
|
|
76
|
-
"test:cache-interceptor": "borp -p \"test/cache-interceptor/*.js\"",
|
|
74
|
+
"test:busboy": "borp --timeout 180000 -p \"test/busboy/*.js\"",
|
|
75
|
+
"test:cache": "borp --timeout 180000 -p \"test/cache/*.js\"",
|
|
76
|
+
"test:cache-interceptor": "borp --timeout 180000 -p \"test/cache-interceptor/*.js\"",
|
|
77
77
|
"test:cache-interceptor:sqlite": "cross-env NODE_OPTIONS=--experimental-sqlite npm run test:cache-interceptor",
|
|
78
|
-
"test:cookies": "borp -p \"test/cookie/*.js\"",
|
|
79
|
-
"test:eventsource": "npm run build:node && borp --expose-gc -p \"test/eventsource/*.js\"",
|
|
78
|
+
"test:cookies": "borp --timeout 180000 -p \"test/cookie/*.js\"",
|
|
79
|
+
"test:eventsource": "npm run build:node && borp --timeout 180000 --expose-gc -p \"test/eventsource/*.js\"",
|
|
80
80
|
"test:fuzzing": "node test/fuzzing/fuzzing.test.js",
|
|
81
81
|
"test:fetch": "npm run build:node && borp --timeout 180000 --expose-gc --concurrency 1 -p \"test/fetch/*.js\" && npm run test:webidl && npm run test:busboy",
|
|
82
|
-
"test:subresource-integrity": "borp -p \"test/subresource-integrity/*.js\"",
|
|
82
|
+
"test:subresource-integrity": "borp --timeout 180000 -p \"test/subresource-integrity/*.js\"",
|
|
83
83
|
"test:h2": "npm run test:h2:core && npm run test:h2:fetch",
|
|
84
|
-
"test:h2:core": "borp -p \"test/+(http2|h2)*.js\"",
|
|
85
|
-
"test:h2:fetch": "npm run build:node && borp -p \"test/fetch/http2*.js\"",
|
|
86
|
-
"test:infra": "borp -p \"test/infra/*.js\"",
|
|
87
|
-
"test:interceptors": "borp -p \"test/interceptors/*.js\"",
|
|
84
|
+
"test:h2:core": "borp --timeout 180000 -p \"test/+(http2|h2)*.js\"",
|
|
85
|
+
"test:h2:fetch": "npm run build:node && borp --timeout 180000 -p \"test/fetch/http2*.js\"",
|
|
86
|
+
"test:infra": "borp --timeout 180000 -p \"test/infra/*.js\"",
|
|
87
|
+
"test:interceptors": "borp --timeout 180000 -p \"test/interceptors/*.js\"",
|
|
88
88
|
"test:jest": "cross-env NODE_V8_COVERAGE= jest",
|
|
89
|
-
"test:unit": "borp --expose-gc -p \"test/*.js\"",
|
|
90
|
-
"test:node-fetch": "borp -p \"test/node-fetch/**/*.js\"",
|
|
91
|
-
"test:node-test": "borp -p \"test/node-test/**/*.js\"",
|
|
92
|
-
"test:tdd": "borp --expose-gc -p \"test/*.js\"",
|
|
93
|
-
"test:tdd:node-test": "borp -p \"test/node-test/**/*.js\" -w",
|
|
89
|
+
"test:unit": "borp --timeout 180000 --expose-gc -p \"test/*.js\"",
|
|
90
|
+
"test:node-fetch": "borp --timeout 180000 -p \"test/node-fetch/**/*.js\"",
|
|
91
|
+
"test:node-test": "borp --timeout 180000 -p \"test/node-test/**/*.js\"",
|
|
92
|
+
"test:tdd": "borp --timeout 180000 --expose-gc -p \"test/*.js\"",
|
|
93
|
+
"test:tdd:node-test": "borp --timeout 180000 -p \"test/node-test/**/*.js\" -w",
|
|
94
94
|
"test:typescript": "tsd && tsc test/imports/undici-import.ts --typeRoots ./types --noEmit && tsc ./types/*.d.ts --noEmit --typeRoots ./types",
|
|
95
|
-
"test:webidl": "borp -p \"test/webidl/*.js\"",
|
|
96
|
-
"test:websocket": "borp -p \"test/websocket/**/*.js\"",
|
|
95
|
+
"test:webidl": "borp --timeout 180000 -p \"test/webidl/*.js\"",
|
|
96
|
+
"test:websocket": "borp --timeout 180000 -p \"test/websocket/**/*.js\"",
|
|
97
97
|
"test:websocket:autobahn": "node test/autobahn/client.js",
|
|
98
98
|
"test:websocket:autobahn:report": "node test/autobahn/report.js",
|
|
99
99
|
"test:wpt:setup": "node test/web-platform-tests/wpt-runner.mjs setup",
|
package/types/client.d.ts
CHANGED
|
@@ -102,6 +102,11 @@ export declare namespace Client {
|
|
|
102
102
|
* @default 524288
|
|
103
103
|
*/
|
|
104
104
|
connectionWindowSize?: number;
|
|
105
|
+
/**
|
|
106
|
+
* @description Time interval between PING frames dispatch
|
|
107
|
+
* @default 60000
|
|
108
|
+
*/
|
|
109
|
+
pingInterval?: number;
|
|
105
110
|
}
|
|
106
111
|
export interface SocketInfo {
|
|
107
112
|
localAddress?: string
|
package/types/dispatcher.d.ts
CHANGED
package/types/webidl.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// These types are not exported, and are only used internally
|
|
2
|
+
import { BufferSource } from 'node:stream/web'
|
|
2
3
|
import * as undici from './index'
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -93,6 +94,11 @@ interface WebidlUtil {
|
|
|
93
94
|
IsResizableArrayBuffer (V: ArrayBufferLike): boolean
|
|
94
95
|
|
|
95
96
|
HasFlag (flag: number, attributes: number): boolean
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* @see https://webidl.spec.whatwg.org/#dfn-get-buffer-source-copy
|
|
100
|
+
*/
|
|
101
|
+
getCopyOfBytesHeldByBufferSource (bufferSource: BufferSource): Uint8Array
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
interface WebidlConverters {
|