Skip to content

Commit 4a79e69

Browse files
committed
Merge branch 'main' into body-wrap
2 parents da031f1 + 64b133c commit 4a79e69

File tree

99 files changed

+5882
-4834
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

99 files changed

+5882
-4834
lines changed

.npmignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,12 @@
1+
*
2+
!lib/**/*
3+
!index.js
4+
!index-fetch.js
5+
6+
# The wasm files are stored as base64 strings in the corresponding .js files
17
lib/llhttp/llhttp_simd.wasm
28
lib/llhttp/llhttp.wasm
9+
10+
!types/**/*
11+
!index.d.ts
12+
!docs/**/*

.taprc

Lines changed: 0 additions & 7 deletions
This file was deleted.

benchmarks/benchmark.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const { Pool, Client, fetch, Agent, setGlobalDispatcher } = require('..')
1010

1111
let nodeFetch
1212
const axios = require('axios')
13+
let superagent
1314
let got
1415

1516
const util = require('node:util')
@@ -85,6 +86,11 @@ const requestAgent = new http.Agent({
8586
maxSockets: connections
8687
})
8788

89+
const superagentAgent = new http.Agent({
90+
keepAlive: true,
91+
maxSockets: connections
92+
})
93+
8894
const undiciOptions = {
8995
path: '/',
9096
method: 'GET',
@@ -318,6 +324,16 @@ if (process.env.PORT) {
318324
}).catch(console.log)
319325
})
320326
}
327+
328+
experiments.superagent = () => {
329+
return makeParallelRequests(resolve => {
330+
superagent.get(dest.url).pipe(new Writable({
331+
write (chunk, encoding, callback) {
332+
callback()
333+
}
334+
})).on('finish', resolve)
335+
})
336+
}
321337
}
322338

323339
async function main () {
@@ -326,6 +342,9 @@ async function main () {
326342
nodeFetch = _nodeFetch.default
327343
const _got = await import('got')
328344
got = _got.default
345+
const _superagent = await import('superagent')
346+
// https://github.com/ladjs/superagent/issues/1540#issue-561464561
347+
superagent = _superagent.agent().use((req) => req.agent(superagentAgent))
329348

330349
cronometro(
331350
experiments,

build/wasm.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ if (EXTERNAL_PATH) {
101101
writeFileSync(join(ROOT, 'loader.js'), `
102102
'use strict'
103103
104+
globalThis.__UNDICI_IS_NODE__ = true
104105
module.exports = require('node:module').createRequire('${EXTERNAL_PATH}/loader.js')('./index-fetch.js')
106+
delete globalThis.__UNDICI_IS_NODE__
105107
`)
106108
}

examples/proxy/proxy.js

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
'use strict'
2+
13
const net = require('node:net')
24
const { pipeline } = require('node:stream')
3-
const createError = require('http-errors')
5+
const { STATUS_CODES } = require('node:http')
46

57
module.exports = async function proxy (ctx, client) {
68
const { req, socket, proxyName } = ctx
@@ -214,13 +216,13 @@ function getHeaders ({
214216
].join(';'))
215217
} else if (forwarded) {
216218
// The forwarded header should not be included in response.
217-
throw new createError.BadGateway()
219+
throw new BadGateway()
218220
}
219221

220222
if (proxyName) {
221223
if (via) {
222224
if (via.split(',').some(name => name.endsWith(proxyName))) {
223-
throw new createError.LoopDetected()
225+
throw new LoopDetected()
224226
}
225227
via += ', '
226228
}
@@ -254,3 +256,63 @@ function printIp (address, port) {
254256
}
255257
return str
256258
}
259+
260+
class BadGateway extends Error {
261+
constructor (message = STATUS_CODES[502]) {
262+
super(message)
263+
}
264+
265+
toString () {
266+
return `BadGatewayError: ${this.message}`
267+
}
268+
269+
get name () {
270+
return 'BadGatewayError'
271+
}
272+
273+
get status () {
274+
return 502
275+
}
276+
277+
get statusCode () {
278+
return 502
279+
}
280+
281+
get expose () {
282+
return false
283+
}
284+
285+
get headers () {
286+
return undefined
287+
}
288+
}
289+
290+
class LoopDetected extends Error {
291+
constructor (message = STATUS_CODES[508]) {
292+
super(message)
293+
}
294+
295+
toString () {
296+
return `LoopDetectedError: ${this.message}`
297+
}
298+
299+
get name () {
300+
return 'LoopDetectedError'
301+
}
302+
303+
get status () {
304+
return 508
305+
}
306+
307+
get statusCode () {
308+
return 508
309+
}
310+
311+
get expose () {
312+
return false
313+
}
314+
315+
get headers () {
316+
return undefined
317+
}
318+
}

lib/client.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2072,13 +2072,13 @@ function writeStream ({ h2stream, body, client, request, socket, contentLength,
20722072
.on('error', onFinished)
20732073

20742074
if (body.errored) {
2075-
queueMicrotask(() => onFinished(body.errored))
2075+
setImmediate(() => onFinished(body.errored))
20762076
} else if (body.readableEnded) {
2077-
queueMicrotask(() => onFinished(null))
2077+
setImmediate(() => onFinished(null))
20782078
}
20792079

20802080
if (body.closeEmitted ?? body.closed) {
2081-
queueMicrotask(onClose)
2081+
setImmediate(onClose)
20822082
}
20832083
}
20842084

lib/core/request.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,10 +162,19 @@ class Request {
162162
processHeader(this, headers[i], headers[i + 1])
163163
}
164164
} else if (headers && typeof headers === 'object') {
165-
const keys = Object.keys(headers)
166-
for (let i = 0; i < keys.length; i++) {
167-
const key = keys[i]
168-
processHeader(this, key, headers[key])
165+
if (headers[Symbol.iterator]) {
166+
for (const header of headers) {
167+
if (!Array.isArray(header) || header.length !== 2) {
168+
throw new InvalidArgumentError('headers must be in key-value pair format')
169+
}
170+
const [key, value] = header
171+
processHeader(this, key, value)
172+
}
173+
} else {
174+
const keys = Object.keys(headers)
175+
for (const key of keys) {
176+
processHeader(this, key, headers[key])
177+
}
169178
}
170179
} else if (headers != null) {
171180
throw new InvalidArgumentError('headers must be an object or an array')

lib/core/util.js

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -438,13 +438,7 @@ const hasToWellFormed = !!String.prototype.toWellFormed
438438
* @param {string} val
439439
*/
440440
function toUSVString (val) {
441-
if (hasToWellFormed) {
442-
return `${val}`.toWellFormed()
443-
} else if (nodeUtil.toUSVString) {
444-
return nodeUtil.toUSVString(val)
445-
}
446-
447-
return `${val}`
441+
return hasToWellFormed ? `${val}`.toWellFormed() : nodeUtil.toUSVString(val)
448442
}
449443

450444
/**

lib/fetch/body.js

Lines changed: 19 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ const { FormData } = require('./formdata')
1515
const { kState } = require('./symbols')
1616
const { webidl } = require('./webidl')
1717
const { Blob, File: NativeFile } = require('node:buffer')
18-
const { kBodyUsed } = require('../core/symbols')
1918
const assert = require('node:assert')
2019
const { isErrored } = require('../core/util')
21-
const { isUint8Array, isArrayBuffer } = require('util/types')
20+
const { isArrayBuffer } = require('util/types')
2221
const { File: UndiciFile } = require('./file')
2322
const { serializeAMimeType } = require('./dataURL')
23+
const { Readable } = require('node:stream')
2424

2525
/** @type {globalThis['File']} */
2626
const File = NativeFile ?? UndiciFile
@@ -291,29 +291,6 @@ function cloneBody (body) {
291291
}
292292
}
293293

294-
async function * consumeBody (body) {
295-
if (body) {
296-
if (isUint8Array(body)) {
297-
yield body
298-
} else {
299-
const stream = body.stream
300-
301-
if (util.isDisturbed(stream)) {
302-
throw new TypeError('The body has already been consumed.')
303-
}
304-
305-
if (stream.locked) {
306-
throw new TypeError('The stream is locked.')
307-
}
308-
309-
// Compat.
310-
stream[kBodyUsed] = true
311-
312-
yield * stream
313-
}
314-
}
315-
}
316-
317294
function throwIfAborted (state) {
318295
if (state.aborted) {
319296
throw new DOMException('The operation was aborted.', 'AbortError')
@@ -328,7 +305,7 @@ function bodyMixinMethods (instance) {
328305
// given a byte sequence bytes: return a Blob whose
329306
// contents are bytes and whose type attribute is this’s
330307
// MIME type.
331-
return specConsumeBody(this, (bytes) => {
308+
return consumeBody(this, (bytes) => {
332309
let mimeType = bodyMimeType(this)
333310

334311
if (mimeType === null) {
@@ -348,21 +325,21 @@ function bodyMixinMethods (instance) {
348325
// of running consume body with this and the following step
349326
// given a byte sequence bytes: return a new ArrayBuffer
350327
// whose contents are bytes.
351-
return specConsumeBody(this, (bytes) => {
328+
return consumeBody(this, (bytes) => {
352329
return new Uint8Array(bytes).buffer
353330
}, instance)
354331
},
355332

356333
text () {
357334
// The text() method steps are to return the result of running
358335
// consume body with this and UTF-8 decode.
359-
return specConsumeBody(this, utf8DecodeBytes, instance)
336+
return consumeBody(this, utf8DecodeBytes, instance)
360337
},
361338

362339
json () {
363340
// The json() method steps are to return the result of running
364341
// consume body with this and parse JSON from bytes.
365-
return specConsumeBody(this, parseJSONFromBytes, instance)
342+
return consumeBody(this, parseJSONFromBytes, instance)
366343
},
367344

368345
async formData () {
@@ -375,16 +352,15 @@ function bodyMixinMethods (instance) {
375352

376353
// If mimeType’s essence is "multipart/form-data", then:
377354
if (mimeType !== null && mimeType.essence === 'multipart/form-data') {
378-
const headers = {}
379-
for (const [key, value] of this.headers) headers[key] = value
380-
381355
const responseFormData = new FormData()
382356

383357
let busboy
384358

385359
try {
386360
busboy = new Busboy({
387-
headers,
361+
headers: {
362+
'content-type': serializeAMimeType(mimeType)
363+
},
388364
preservePath: true
389365
})
390366
} catch (err) {
@@ -427,8 +403,10 @@ function bodyMixinMethods (instance) {
427403
busboy.on('error', (err) => reject(new TypeError(err)))
428404
})
429405

430-
if (this.body !== null) for await (const chunk of consumeBody(this[kState].body)) busboy.write(chunk)
431-
busboy.end()
406+
if (this.body !== null) {
407+
Readable.from(this[kState].body.stream).pipe(busboy)
408+
}
409+
432410
await busboyResolve
433411

434412
return responseFormData
@@ -442,20 +420,17 @@ function bodyMixinMethods (instance) {
442420
// application/x-www-form-urlencoded parser will keep the BOM.
443421
// https://url.spec.whatwg.org/#concept-urlencoded-parser
444422
// Note that streaming decoder is stateful and cannot be reused
445-
const streamingDecoder = new TextDecoder('utf-8', { ignoreBOM: true })
423+
const stream = this[kState].body.stream.pipeThrough(new TextDecoderStream('utf-8', { ignoreBOM: true }))
446424

447-
for await (const chunk of consumeBody(this[kState].body)) {
448-
if (!isUint8Array(chunk)) {
449-
throw new TypeError('Expected Uint8Array chunk')
450-
}
451-
text += streamingDecoder.decode(chunk, { stream: true })
425+
for await (const chunk of stream) {
426+
text += chunk
452427
}
453-
text += streamingDecoder.decode()
428+
454429
entries = new URLSearchParams(text)
455430
} catch (err) {
456431
// istanbul ignore next: Unclear when new URLSearchParams can fail on a string.
457432
// 2. If entries is failure, then throw a TypeError.
458-
throw new TypeError(undefined, { cause: err })
433+
throw new TypeError(err)
459434
}
460435

461436
// 3. Return a new FormData object whose entries are entries.
@@ -493,7 +468,7 @@ function mixinBody (prototype) {
493468
* @param {(value: unknown) => unknown} convertBytesToJSValue
494469
* @param {Response|Request} instance
495470
*/
496-
async function specConsumeBody (object, convertBytesToJSValue, instance) {
471+
async function consumeBody (object, convertBytesToJSValue, instance) {
497472
webidl.brandCheck(object, instance)
498473

499474
throwIfAborted(object[kState])

0 commit comments

Comments
 (0)