Skip to content

Commit

Permalink
fetch: implement new spec changes (nodejs#1721)
Browse files Browse the repository at this point in the history
  • Loading branch information
KhafraDev authored and metcoder95 committed Dec 26, 2022
1 parent f5c8bb8 commit 10afbbf
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 83 deletions.
99 changes: 46 additions & 53 deletions lib/fetch/body.js
Expand Up @@ -2,7 +2,7 @@

const Busboy = require('busboy')
const util = require('../core/util')
const { ReadableStreamFrom, toUSVString, isBlobLike } = require('./util')
const { ReadableStreamFrom, toUSVString, isBlobLike, isReadableStreamLike, readableStreamClose } = require('./util')
const { FormData } = require('./formdata')
const { kState } = require('./symbols')
const { webidl } = require('./webidl')
Expand All @@ -19,33 +19,53 @@ const { parseMIMEType, serializeAMimeType } = require('./dataURL')
/** @type {globalThis['ReadableStream']} */
let ReadableStream

async function * blobGen (blob) {
yield * blob.stream()
}

// https://fetch.spec.whatwg.org/#concept-bodyinit-extract
function extractBody (object, keepalive = false) {
if (!ReadableStream) {
ReadableStream = require('stream/web').ReadableStream
}

// 1. Let stream be object if object is a ReadableStream object.
// Otherwise, let stream be a new ReadableStream, and set up stream.
// 1. Let stream be null.
let stream = null

// 2. Let action be null.
// 2. If object is a ReadableStream object, then set stream to object.
if (object instanceof ReadableStream) {
stream = object
} else if (isBlobLike(object)) {
// 3. Otherwise, if object is a Blob object, set stream to the
// result of running object’s get stream.
stream = object.stream()
} else {
// 4. Otherwise, set stream to a new ReadableStream object, and set
// up stream.
stream = new ReadableStream({
async pull (controller) {
controller.enqueue(
typeof source === 'string' ? new TextEncoder().encode(source) : source
)
queueMicrotask(() => readableStreamClose(controller))
},
start () {},
type: undefined
})
}

// 5. Assert: stream is a ReadableStream object.
assert(isReadableStreamLike(stream))

// 6. Let action be null.
let action = null

// 3. Let source be null.
// 7. Let source be null.
let source = null

// 4. Let length be null.
// 8. Let length be null.
let length = null

// 5. Let Content-Type be null.
let contentType = null
// 9. Let type be null.
let type = null

// 6. Switch on object:
// 10. Switch on object:
if (object == null) {
// Note: The IDL processor cannot handle this situation. See
// https://crbug.com/335871.
Expand All @@ -60,8 +80,8 @@ function extractBody (object, keepalive = false) {
// Set source to the result of running the application/x-www-form-urlencoded serializer with object’s list.
source = object.toString()

// Set Content-Type to `application/x-www-form-urlencoded;charset=UTF-8`.
contentType = 'application/x-www-form-urlencoded;charset=UTF-8'
// Set type to `application/x-www-form-urlencoded;charset=UTF-8`.
type = 'application/x-www-form-urlencoded;charset=UTF-8'
} else if (isArrayBuffer(object)) {
// BufferSource/ArrayBuffer

Expand Down Expand Up @@ -104,7 +124,7 @@ function extractBody (object, keepalive = false) {
}\r\n\r\n`
)

yield * blobGen(value)
yield * value.stream()

yield enc.encode('\r\n')
}
Expand All @@ -119,26 +139,23 @@ function extractBody (object, keepalive = false) {
// Set length to unclear, see html/6424 for improving this.
// TODO

// Set Content-Type to `multipart/form-data; boundary=`,
// Set type to `multipart/form-data; boundary=`,
// followed by the multipart/form-data boundary string generated
// by the multipart/form-data encoding algorithm.
contentType = 'multipart/form-data; boundary=' + boundary
type = 'multipart/form-data; boundary=' + boundary
} else if (isBlobLike(object)) {
// Blob

// Set action to this step: read object.
action = blobGen

// Set source to object.
source = object

// Set length to object’s size.
length = object.size

// If object’s type attribute is not the empty byte sequence, set
// Content-Type to its value.
// type to its value.
if (object.type) {
contentType = object.type
type = object.type
}
} else if (typeof object[Symbol.asyncIterator] === 'function') {
// If keepalive is true, then throw a TypeError.
Expand All @@ -160,17 +177,17 @@ function extractBody (object, keepalive = false) {
// TODO: scalar value string?
// TODO: else?
source = toUSVString(object)
contentType = 'text/plain;charset=UTF-8'
type = 'text/plain;charset=UTF-8'
}

// 7. If source is a byte sequence, then set action to a
// 11. If source is a byte sequence, then set action to a
// step that returns source and length to source’s length.
// TODO: What is a "byte sequence?"
if (typeof source === 'string' || util.isBuffer(source)) {
length = Buffer.byteLength(source)
}

// 8. If action is non-null, then run these steps in in parallel:
// 12. If action is non-null, then run these steps in in parallel:
if (action != null) {
// Run action.
let iterator
Expand Down Expand Up @@ -200,38 +217,14 @@ function extractBody (object, keepalive = false) {
},
type: undefined
})
} else if (!stream) {
// TODO: Spec doesn't say anything about this?
stream = new ReadableStream({
start () {},
async pull (controller) {
controller.enqueue(
typeof source === 'string' ? new TextEncoder().encode(source) : source
)
queueMicrotask(() => {
try {
controller.close()
} catch (err) {
// TODO(@KhafraDev): this error is thrown in
// response-stream-disturbed-4.any.js - investigate
// why it does so.

if (!/Controller is already closed/.test(err)) {
throw err
}
}
})
},
type: undefined
})
}

// 9. Let body be a body whose stream is stream, source is source,
// 13. Let body be a body whose stream is stream, source is source,
// and length is length.
const body = { stream, source, length }

// 10. Return body and Content-Type.
return [body, contentType]
// 14. Return (body, type).
return [body, type]
}

// https://fetch.spec.whatwg.org/#bodyinit-safely-extract
Expand Down
60 changes: 31 additions & 29 deletions lib/fetch/index.js
Expand Up @@ -35,11 +35,12 @@ const {
isCancelled,
isAborted,
isErrorLike,
fullyReadBody
fullyReadBody,
readableStreamClose
} = require('./util')
const { kState, kHeaders, kGuard, kRealm } = require('./symbols')
const assert = require('assert')
const { safelyExtractBody, extractBody } = require('./body')
const { safelyExtractBody } = require('./body')
const {
redirectStatus,
nullBodyStatus,
Expand Down Expand Up @@ -427,8 +428,8 @@ function fetching ({
crossOriginIsolatedCapability
}

// 7. If request’s body is a byte sequence, then set request’s body to the
// first return value of safely extracting request’s body.
// 7. If request’s body is a byte sequence, then set request’s body to
// request’s body as a body.
// NOTE: Since fetching is only called from fetch, body should already be
// extracted.
assert(!request.body || request.body.stream)
Expand Down Expand Up @@ -758,8 +759,7 @@ async function mainFetch (fetchParams, recursive = false) {
return
}

// 2. Set response’s body to the first return value of safely
// extracting bytes.
// 2. Set response’s body to bytes as a body.
response.body = safelyExtractBody(bytes)[0]

// 3. Run fetch finale given fetchParams and response.
Expand Down Expand Up @@ -787,7 +787,7 @@ async function schemeFetch (fetchParams) {
case 'about:': {
// If request’s current URL’s path is the string "blank", then return a new response
// whose status message is `OK`, header list is « (`Content-Type`, `text/html;charset=utf-8`) »,
// and body is the empty byte sequence.
// and body is the empty byte sequence as a body.

// Otherwise, return a network error.
return makeNetworkError('about scheme is not supported')
Expand All @@ -809,27 +809,36 @@ async function schemeFetch (fetchParams) {

const blob = resolveObjectURL(currentURL.toString())

// 2. If request’s method is not `GET` or blob is not a Blob object, then return a network error. [FILEAPI]
// 2. If request’s method is not `GET` or blob is not a Blob object, then return a network error.
if (request.method !== 'GET' || !isBlobLike(blob)) {
return makeNetworkError('invalid method')
}

// 3. Let response be a new response whose status message is `OK`.
const response = makeResponse({ statusText: 'OK', urlList: [currentURL] })
// 3. Let bodyWithType be the result of safely extracting blobURLEntry’s object.
const bodyWithType = safelyExtractBody(blob)

// 4. Append (`Content-Length`, blob’s size attribute value) to response’s header list.
response.headersList.set('content-length', `${blob.size}`)
// 4. Let body be bodyWithType’s body.
const body = bodyWithType[0]

// 5. Append (`Content-Type`, blob’s type attribute value) to response’s header list.
response.headersList.set('content-type', blob.type)
// 5. Let length be body’s length, serialized and isomorphic encoded.
const length = `${body.length}`

// 6. Set response’s body to the result of performing the read operation on blob.
// TODO (fix): This needs to read?
response.body = extractBody(blob)[0]
// 6. Let type be bodyWithType’s type if it is non-null; otherwise the empty byte sequence.
const type = bodyWithType[1] ?? ''

// 7. Return response.
return response
// 7. Return a new response whose status message is `OK`, header list is
// « (`Content-Length`, length), (`Content-Type`, type) », and body is body.
const response = makeResponse({
statusText: 'OK',
headersList: [
['content-length', length],
['content-type', type]
]
})

response.body = body

return response
// 2. If aborted, then return the appropriate network error for fetchParams.
// TODO
}
Expand All @@ -850,13 +859,13 @@ async function schemeFetch (fetchParams) {

// 4. Return a response whose status message is `OK`,
// header list is « (`Content-Type`, mimeType) »,
// and body is dataURLStruct’s body.
// and body is dataURLStruct’s body as a body.
return makeResponse({
statusText: 'OK',
headersList: [
['content-type', mimeType]
],
body: extractBody(dataURLStruct.body)[0]
body: safelyExtractBody(dataURLStruct.body)[0]
})
}
case 'file:': {
Expand Down Expand Up @@ -1841,14 +1850,7 @@ async function httpNetworkFetch (
// body is done normally and stream is readable, then close
// stream, finalize response for fetchParams and response, and
// abort these in-parallel steps.
try {
fetchParams.controller.controller.close()
} catch (err) {
// TODO (fix): How/Why can this happen? Do we have a bug?
if (!/Controller is already closed/.test(err)) {
throw err
}
}
readableStreamClose(fetchParams.controller.controller)

finalizeResponse(fetchParams, response)

Expand Down
32 changes: 31 additions & 1 deletion lib/fetch/util.js
Expand Up @@ -842,6 +842,34 @@ async function fullyReadBody (body, processBody, processBodyError) {
// 5. React to promise with fulfilledSteps and rejectedSteps.
}

/** @type {ReadableStream} */
let ReadableStream = globalThis.ReadableStream

function isReadableStreamLike (stream) {
if (!ReadableStream) {
ReadableStream = require('stream/web').ReadableStream
}

return stream instanceof ReadableStream || (
stream[Symbol.toStringTag] === 'ReadableStream' &&
typeof stream.tee === 'function'
)
}

/**
* @param {ReadableStreamController<Uint8Array>} controller
*/
function readableStreamClose (controller) {
try {
controller.close()
} catch (err) {
// TODO: add comment explaining why this error occurs.
if (!err.message.includes('Controller is already closed')) {
throw err
}
}
}

/**
* Fetch supports node >= 16.8.0, but Object.hasOwn was added in v16.9.0.
*/
Expand Down Expand Up @@ -882,5 +910,7 @@ module.exports = {
hasOwn,
isErrorLike,
fullyReadBody,
bytesMatch
bytesMatch,
isReadableStreamLike,
readableStreamClose
}

0 comments on commit 10afbbf

Please sign in to comment.