diff --git a/lib/fetch/body.js b/lib/fetch/body.js index 722ee68c792..7de30264d68 100644 --- a/lib/fetch/body.js +++ b/lib/fetch/body.js @@ -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') @@ -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. @@ -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 @@ -104,7 +124,7 @@ function extractBody (object, keepalive = false) { }\r\n\r\n` ) - yield * blobGen(value) + yield * value.stream() yield enc.encode('\r\n') } @@ -119,16 +139,13 @@ 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 @@ -136,9 +153,9 @@ function extractBody (object, keepalive = false) { 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. @@ -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 @@ -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 diff --git a/lib/fetch/index.js b/lib/fetch/index.js index 53751c41ae6..4bb0173551e 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -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, @@ -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) @@ -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. @@ -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') @@ -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 } @@ -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:': { @@ -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) diff --git a/lib/fetch/util.js b/lib/fetch/util.js index cf485b7daf1..c71ab16eec1 100644 --- a/lib/fetch/util.js +++ b/lib/fetch/util.js @@ -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} 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. */ @@ -882,5 +910,7 @@ module.exports = { hasOwn, isErrorLike, fullyReadBody, - bytesMatch + bytesMatch, + isReadableStreamLike, + readableStreamClose }