Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement spec changes from a while ago #2676

Merged
merged 2 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 25 additions & 17 deletions lib/fetch/body.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,19 @@ const {
isReadableStreamLike,
readableStreamClose,
createDeferredPromise,
fullyReadBody
fullyReadBody,
extractMimeType
} = require('./util')
const { FormData } = require('./formdata')
const { kState } = require('./symbols')
const { webidl } = require('./webidl')
const { Blob, File: NativeFile } = require('node:buffer')
const { kBodyUsed, kHeadersList } = require('../core/symbols')
const { kBodyUsed } = require('../core/symbols')
const assert = require('node:assert')
const { isErrored } = require('../core/util')
const { isUint8Array, isArrayBuffer } = require('util/types')
const { File: UndiciFile } = require('./file')
const { parseMIMEType, serializeAMimeType } = require('./dataURL')
const { serializeAMimeType } = require('./dataURL')

/** @type {globalThis['File']} */
const File = NativeFile ?? UndiciFile
Expand Down Expand Up @@ -330,7 +331,7 @@ function bodyMixinMethods (instance) {
return specConsumeBody(this, (bytes) => {
let mimeType = bodyMimeType(this)

if (mimeType === 'failure') {
if (mimeType === null) {
mimeType = ''
} else if (mimeType) {
mimeType = serializeAMimeType(mimeType)
Expand Down Expand Up @@ -369,12 +370,11 @@ function bodyMixinMethods (instance) {

throwIfAborted(this[kState])

const contentType = this.headers[kHeadersList].get('content-type', true)

const mimeType = contentType !== null ? parseMIMEType(contentType) : 'failure'
// 1. Let mimeType be the result of get the MIME type with this.
const mimeType = bodyMimeType(this)

// If mimeType’s essence is "multipart/form-data", then:
if (mimeType !== 'failure' && mimeType.essence === 'multipart/form-data') {
if (mimeType !== null && mimeType.essence === 'multipart/form-data') {
const headers = {}
for (const [key, value] of this.headers) headers[key] = value

Expand Down Expand Up @@ -432,7 +432,7 @@ function bodyMixinMethods (instance) {
await busboyResolve

return responseFormData
} else if (mimeType !== 'failure' && mimeType.essence === 'application/x-www-form-urlencoded') {
} else if (mimeType !== null && mimeType.essence === 'application/x-www-form-urlencoded') {
// Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then:

// 1. Let entries be the result of parsing bytes.
Expand Down Expand Up @@ -581,17 +581,25 @@ function parseJSONFromBytes (bytes) {

/**
* @see https://fetch.spec.whatwg.org/#concept-body-mime-type
* @param {import('./response').Response|import('./request').Request} object
* @param {import('./response').Response|import('./request').Request} requestOrResponse
*/
function bodyMimeType (object) {
const { headersList } = object[kState]
const contentType = headersList.get('content-type')

if (contentType === null) {
return 'failure'
function bodyMimeType (requestOrResponse) {
// 1. Let headers be null.
// 2. If requestOrResponse is a Request object, then set headers to requestOrResponse’s request’s header list.
// 3. Otherwise, set headers to requestOrResponse’s response’s header list.
/** @type {import('./headers').HeadersList} */
const headers = requestOrResponse[kState].headersList

// 4. Let mimeType be the result of extracting a MIME type from headers.
const mimeType = extractMimeType(headers)

// 5. If mimeType is failure, then return null.
if (mimeType === 'failure') {
return null
}

return parseMIMEType(contentType)
// 6. Return mimeType.
return mimeType
}

module.exports = {
Expand Down
112 changes: 96 additions & 16 deletions lib/fetch/dataURL.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const assert = require('node:assert')
const { isomorphicDecode } = require('./util')

const encoder = new TextEncoder()

Expand Down Expand Up @@ -604,18 +603,7 @@ function isHTTPWhiteSpace (char) {
* @param {boolean} [trailing=true]
*/
function removeHTTPWhitespace (str, leading = true, trailing = true) {
let lead = 0
let trail = str.length - 1

if (leading) {
while (lead < str.length && isHTTPWhiteSpace(str.charCodeAt(lead))) lead++
}

if (trailing) {
while (trail > 0 && isHTTPWhiteSpace(str.charCodeAt(trail))) trail--
}

return lead === 0 && trail === str.length - 1 ? str : str.slice(lead, trail + 1)
return removeChars(str, leading, trailing, isHTTPWhiteSpace)
}

/**
Expand All @@ -634,20 +622,110 @@ function isASCIIWhitespace (char) {
* @param {boolean} [trailing=true]
*/
function removeASCIIWhitespace (str, leading = true, trailing = true) {
return removeChars(str, leading, trailing, isASCIIWhitespace)
}

/**
*
* @param {string} str
* @param {boolean} leading
* @param {boolean} trailing
* @param {(charCode: number) => boolean} predicate
* @returns
*/
function removeChars (str, leading, trailing, predicate) {
let lead = 0
let trail = str.length - 1

if (leading) {
while (lead < str.length && isASCIIWhitespace(str.charCodeAt(lead))) lead++
while (lead < str.length && predicate(str.charCodeAt(lead))) lead++
}

if (trailing) {
while (trail > 0 && isASCIIWhitespace(str.charCodeAt(trail))) trail--
while (trail > 0 && predicate(str.charCodeAt(trail))) trail--
}

return lead === 0 && trail === str.length - 1 ? str : str.slice(lead, trail + 1)
}

/**
* @see https://infra.spec.whatwg.org/#isomorphic-decode
* @param {Uint8Array} input
* @returns {string}
*/
function isomorphicDecode (input) {
// 1. To isomorphic decode a byte sequence input, return a string whose code point
// length is equal to input’s length and whose code points have the same values
// as the values of input’s bytes, in the same order.
const length = input.length
if ((2 << 15) - 1 > length) {
return String.fromCharCode.apply(null, input)
}
let result = ''; let i = 0
let addition = (2 << 15) - 1
while (i < length) {
if (i + addition > length) {
addition = length - i
}
result += String.fromCharCode.apply(null, input.subarray(i, i += addition))
}
return result
}

/**
* @see https://mimesniff.spec.whatwg.org/#minimize-a-supported-mime-type
* @param {Exclude<ReturnType<typeof parseMIMEType>, 'failure'>} mimeType
*/
function minimizeSupportedMimeType (mimeType) {
switch (mimeType.essence) {
case 'application/ecmascript':
case 'application/javascript':
case 'application/x-ecmascript':
case 'application/x-javascript':
case 'text/ecmascript':
case 'text/javascript':
case 'text/javascript1.0':
case 'text/javascript1.1':
case 'text/javascript1.2':
case 'text/javascript1.3':
case 'text/javascript1.4':
case 'text/javascript1.5':
case 'text/jscript':
case 'text/livescript':
case 'text/x-ecmascript':
case 'text/x-javascript':
// 1. If mimeType is a JavaScript MIME type, then return "text/javascript".
return 'text/javascript'
case 'application/json':
case 'text/json':
// 2. If mimeType is a JSON MIME type, then return "application/json".
return 'application/json'
case 'image/svg+xml':
// 3. If mimeType’s essence is "image/svg+xml", then return "image/svg+xml".
return 'image/svg+xml'
case 'text/xml':
case 'application/xml':
// 4. If mimeType is an XML MIME type, then return "application/xml".
return 'application/xml'
}

// 2. If mimeType is a JSON MIME type, then return "application/json".
if (mimeType.subtype.endsWith('+json')) {
return 'application/json'
}

// 4. If mimeType is an XML MIME type, then return "application/xml".
if (mimeType.subtype.endsWith('+xml')) {
return 'application/xml'
}

// 5. If mimeType is supported by the user agent, then return mimeType’s essence.
// Technically, node doesn't support any mimetypes.

// 6. Return the empty string.
return ''
}

module.exports = {
dataURLProcessor,
URLSerializer,
Expand All @@ -656,5 +734,7 @@ module.exports = {
stringPercentDecode,
parseMIMEType,
collectAnHTTPQuotedString,
serializeAMimeType
serializeAMimeType,
removeChars,
minimizeSupportedMimeType
}
9 changes: 5 additions & 4 deletions lib/fetch/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ const {
clampAndCoarsenConnectionTimingInfo,
simpleRangeHeaderValue,
buildContentRange,
createInflate
createInflate,
extractMimeType
} = require('./util')
const { kState } = require('./symbols')
const assert = require('node:assert')
Expand All @@ -59,7 +60,7 @@ const {
const EE = require('node:events')
const { Readable, pipeline } = require('node:stream')
const { addAbortListener, isErrored, isReadable, nodeMajor, nodeMinor, bufferToLowerCasedHeaderName } = require('../core/util')
const { dataURLProcessor, serializeAMimeType, parseMIMEType } = require('./dataURL')
const { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = require('./dataURL')
const { getGlobalDispatcher } = require('../global')
const { webidl } = require('./webidl')
const { STATUS_CODES } = require('node:http')
Expand Down Expand Up @@ -1027,11 +1028,11 @@ function fetchFinale (fetchParams, response) {
responseStatus = response.status

// 2. Let mimeType be the result of extracting a MIME type from response’s header list.
const mimeType = parseMIMEType(response.headersList.get('content-type', true)) // TODO: fix
const mimeType = extractMimeType(response.headersList)

// 3. If mimeType is not failure, then set bodyInfo’s content type to the result of minimizing a supported MIME type given mimeType.
if (mimeType !== 'failure') {
// TODO
bodyInfo.contentType = minimizeSupportedMimeType(mimeType)
}
}

Expand Down