Skip to content

Commit

Permalink
Merge branch 'nodejs:main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
arontsang authored Apr 13, 2022
2 parents ed4aacc + 4b52053 commit 6896e5c
Show file tree
Hide file tree
Showing 19 changed files with 479 additions and 167 deletions.
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,15 @@ for await (const data of body) {
console.log('trailers', trailers)
```

Using [the body mixin from the Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin).
## Body Mixins

The `body` mixins are the most common way to format the request/response body. Mixins include:

- [`.formData()`](https://fetch.spec.whatwg.org/#dom-body-formdata)
- [`.json()`](https://fetch.spec.whatwg.org/#dom-body-json)
- [`.text()`](https://fetch.spec.whatwg.org/#dom-body-text)

Example usage:

```js
import { request } from 'undici'
Expand All @@ -83,6 +91,12 @@ console.log('data', await body.json())
console.log('trailers', trailers)
```

_Note: Once a mixin has been called then the body cannot be reused, thus calling additional mixins on `.body`, e.g. `.body.json(); .body.text()` will result in an error `TypeError: unusable` being thrown and returned through the `Promise` rejection._

Should you need to access the `body` in plain-text after using a mixin, the best practice is to use the `.text()` mixin first, and manually parse the text to the desired format.

For more information about their behavior, please reference the body mixin from the [Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin).

## Common API Methods

This section documents our most commonly used API methods. Additional APIs are documented in their own files within the [docs](./docs/) folder and are accessible via the navigation list on the left side of the docs site.
Expand Down
26 changes: 24 additions & 2 deletions lib/core/request.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ const kHandler = Symbol('handler')

const channels = {}

let extractBody

const [nodeMajor, nodeMinor] = process.version
.slice(1) // remove 'v'
.split('.', 2)
.map(v => Number(v))

try {
const diagnosticsChannel = require('diagnostics_channel')
channels.create = diagnosticsChannel.channel('undici:request:create')
Expand Down Expand Up @@ -79,7 +86,7 @@ class Request {
this.body = body.byteLength ? body : null
} else if (typeof body === 'string') {
this.body = body.length ? Buffer.from(body) : null
} else if (util.isIterable(body) || util.isBlobLike(body)) {
} else if (util.isFormDataLike(body) || util.isIterable(body) || util.isBlobLike(body)) {
this.body = body
} else {
throw new InvalidArgumentError('body must be a string, a Buffer, a Readable stream, an iterable, or an async iterable')
Expand Down Expand Up @@ -126,7 +133,22 @@ class Request {
throw new InvalidArgumentError('headers must be an object or an array')
}

if (util.isBlobLike(body) && this.contentType == null && body.type) {
if (util.isFormDataLike(this.body)) {
if (nodeMajor < 16 || (nodeMajor === 16 && nodeMinor < 5)) {
throw new InvalidArgumentError('Form-Data bodies are only supported in node v16.5 and newer.')
}

if (!extractBody) {
extractBody = require('../fetch/body.js').extractBody
}

const [bodyStream, contentType] = extractBody(body)
if (this.contentType == null) {
this.contentType = contentType
this.headers += `content-type: ${contentType}\r\n`
}
this.body = bodyStream.stream
} else if (util.isBlobLike(body) && this.contentType == null && body.type) {
this.contentType = body.type
this.headers += `content-type: ${body.type}\r\n`
}
Expand Down
7 changes: 6 additions & 1 deletion lib/core/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,10 @@ function ReadableStreamFrom (iterable) {
)
}

function isFormDataLike (chunk) {
return chunk && chunk.constructor && chunk.constructor.name === 'FormData'
}

const kEnumerableProperty = Object.create(null)
kEnumerableProperty.enumerable = true

Expand Down Expand Up @@ -352,5 +356,6 @@ module.exports = {
ReadableStreamFrom,
isBuffer,
validateHandler,
getSocketInfo
getSocketInfo,
isFormDataLike
}
2 changes: 1 addition & 1 deletion lib/fetch/body.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ function extractBody (object, keepalive = false) {

// Set source to a copy of the bytes held by object.
source = new Uint8Array(object)
} else if (object instanceof FormData) {
} else if (util.isFormDataLike(object)) {
const boundary = '----formdata-undici-' + Math.random()
const prefix = `--${boundary}\r\nContent-Disposition: form-data`

Expand Down
148 changes: 75 additions & 73 deletions lib/fetch/headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,8 @@ const {
forbiddenResponseHeaderNames
} = require('./constants')

function binarySearch (arr, val) {
let low = 0
let high = Math.floor(arr.length / 2)

while (high > low) {
const mid = (high + low) >>> 1

if (val.localeCompare(arr[mid * 2]) > 0) {
low = mid + 1
} else {
high = mid
}
}

return low * 2
}
const kHeadersMap = Symbol('headers map')
const kHeadersSortedMap = Symbol('headers map sorted')

function normalizeAndValidateHeaderName (name) {
if (name === undefined) {
Expand Down Expand Up @@ -91,64 +77,74 @@ function fill (headers, object) {
}
}

// TODO: Composition over inheritence? Or helper methods?
class HeadersList extends Array {
class HeadersList {
constructor (init) {
if (init instanceof HeadersList) {
this[kHeadersMap] = new Map(init[kHeadersMap])
this[kHeadersSortedMap] = init[kHeadersSortedMap]
} else {
this[kHeadersMap] = new Map(init)
this[kHeadersSortedMap] = null
}
}

append (name, value) {
this[kHeadersSortedMap] = null

const normalizedName = normalizeAndValidateHeaderName(name)
const normalizedValue = normalizeAndValidateHeaderValue(name, value)

const index = binarySearch(this, normalizedName)
const exists = this[kHeadersMap].get(normalizedName)

if (this[index] === normalizedName) {
this[index + 1] += `, ${normalizedValue}`
if (exists) {
this[kHeadersMap].set(normalizedName, `${exists}, ${normalizedValue}`)
} else {
this.splice(index, 0, normalizedName, normalizedValue)
this[kHeadersMap].set(normalizedName, `${normalizedValue}`)
}
}

delete (name) {
set (name, value) {
this[kHeadersSortedMap] = null

const normalizedName = normalizeAndValidateHeaderName(name)
return this[kHeadersMap].set(normalizedName, value)
}

const index = binarySearch(this, normalizedName)
delete (name) {
this[kHeadersSortedMap] = null

if (this[index] === normalizedName) {
this.splice(index, 2)
}
const normalizedName = normalizeAndValidateHeaderName(name)
return this[kHeadersMap].delete(normalizedName)
}

get (name) {
const normalizedName = normalizeAndValidateHeaderName(name)

const index = binarySearch(this, normalizedName)

if (this[index] === normalizedName) {
return this[index + 1]
}

return null
return this[kHeadersMap].get(normalizedName) ?? null
}

has (name) {
const normalizedName = normalizeAndValidateHeaderName(name)
return this[kHeadersMap].has(normalizedName)
}

const index = binarySearch(this, normalizedName)
keys () {
return this[kHeadersMap].keys()
}

return this[index] === normalizedName
values () {
return this[kHeadersMap].values()
}

set (name, value) {
const normalizedName = normalizeAndValidateHeaderName(name)
const normalizedValue = normalizeAndValidateHeaderValue(name, value)
entries () {
return this[kHeadersMap].entries()
}

const index = binarySearch(this, normalizedName)
if (this[index] === normalizedName) {
this[index + 1] = normalizedValue
} else {
this.splice(index, 0, normalizedName, normalizedValue)
}
[Symbol.iterator] () {
return this[kHeadersMap][Symbol.iterator]()
}
}

// https://fetch.spec.whatwg.org/#headers-class
class Headers {
constructor (...args) {
if (
Expand All @@ -161,7 +157,6 @@ class Headers {
)
}
const init = args.length >= 1 ? args[0] ?? {} : {}

this[kHeadersList] = new HeadersList()

// The new Headers(init) constructor steps are:
Expand Down Expand Up @@ -287,46 +282,60 @@ class Headers {
)
}

const normalizedName = normalizeAndValidateHeaderName(String(args[0]))

if (this[kGuard] === 'immutable') {
throw new TypeError('immutable')
} else if (
this[kGuard] === 'request' &&
forbiddenHeaderNames.includes(normalizedName)
forbiddenHeaderNames.includes(String(args[0]).toLocaleLowerCase())
) {
return
} else if (this[kGuard] === 'request-no-cors') {
// TODO
} else if (
this[kGuard] === 'response' &&
forbiddenResponseHeaderNames.includes(normalizedName)
forbiddenResponseHeaderNames.includes(String(args[0]).toLocaleLowerCase())
) {
return
}

return this[kHeadersList].set(String(args[0]), String(args[1]))
}

* keys () {
const clone = this[kHeadersList].slice()
for (let index = 0; index < clone.length; index += 2) {
yield clone[index]
get [kHeadersSortedMap] () {
this[kHeadersList][kHeadersSortedMap] ??= new Map([...this[kHeadersList]].sort((a, b) => a[0] < b[0] ? -1 : 1))
return this[kHeadersList][kHeadersSortedMap]
}

keys () {
if (!(this instanceof Headers)) {
throw new TypeError('Illegal invocation')
}

return this[kHeadersSortedMap].keys()
}

* values () {
const clone = this[kHeadersList].slice()
for (let index = 1; index < clone.length; index += 2) {
yield clone[index]
values () {
if (!(this instanceof Headers)) {
throw new TypeError('Illegal invocation')
}

return this[kHeadersSortedMap].values()
}

* entries () {
const clone = this[kHeadersList].slice()
for (let index = 0; index < clone.length; index += 2) {
yield [clone[index], clone[index + 1]]
entries () {
if (!(this instanceof Headers)) {
throw new TypeError('Illegal invocation')
}

return this[kHeadersSortedMap].entries()
}

[Symbol.iterator] () {
if (!(this instanceof Headers)) {
throw new TypeError('Illegal invocation')
}

return this[kHeadersSortedMap]
}

forEach (...args) {
Expand All @@ -346,15 +355,9 @@ class Headers {
const callback = args[0]
const thisArg = args[1]

const clone = this[kHeadersList].slice()
for (let index = 0; index < clone.length; index += 2) {
callback.call(
thisArg,
clone[index + 1],
clone[index],
this
)
}
this[kHeadersSortedMap].forEach((value, index) => {
callback.apply(thisArg, [value, index, this])
})
}

[Symbol.for('nodejs.util.inspect.custom')] () {
Expand Down Expand Up @@ -384,7 +387,6 @@ module.exports = {
fill,
Headers,
HeadersList,
binarySearch,
normalizeAndValidateHeaderName,
normalizeAndValidateHeaderValue
}
6 changes: 3 additions & 3 deletions lib/fetch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,7 @@ async function schemeFetch (fetchParams) {
const resp = makeResponse({
statusText: 'OK',
headersList: [
'content-type', 'text/html;charset=utf-8'
['content-type', 'text/html;charset=utf-8']
]
})

Expand Down Expand Up @@ -871,7 +871,7 @@ async function schemeFetch (fetchParams) {
return makeResponse({
statusText: 'OK',
headersList: [
'content-type', contentType
['content-type', contentType]
],
body: extractBody(dataURLStruct.body)[0]
})
Expand Down Expand Up @@ -1919,7 +1919,7 @@ async function httpNetworkFetch (
origin: url.origin,
method: request.method,
body: fetchParams.controller.dispatcher[kIsMockActive] ? request.body && request.body.source : body,
headers: request.headersList,
headers: [...request.headersList].flat(),
maxRedirections: 0
},
{
Expand Down
Loading

0 comments on commit 6896e5c

Please sign in to comment.