From 8eaa6ddec13b1801c1e598bc0cd8ae8215a50339 Mon Sep 17 00:00:00 2001 From: tsctx <91457664+tsctx@users.noreply.github.com> Date: Fri, 1 Mar 2024 23:41:20 +0900 Subject: [PATCH] WIP --- lib/dispatcher/client-h1.js | 2 +- lib/web/fetch/body.js | 41 +++++++++++++++++++++++----------- lib/web/fetch/constants.js | 1 + lib/web/fetch/index.js | 12 ++++++---- lib/web/fetch/request.js | 22 +++++++++++++++---- lib/web/fetch/response.js | 44 +++++++++++++++++++++++++++++++++++-- lib/web/fetch/util.js | 5 +++-- 7 files changed, 101 insertions(+), 26 deletions(-) diff --git a/lib/dispatcher/client-h1.js b/lib/dispatcher/client-h1.js index 46b63087826..043ce6e6b78 100644 --- a/lib/dispatcher/client-h1.js +++ b/lib/dispatcher/client-h1.js @@ -857,7 +857,7 @@ function writeH1 (client, request) { extractBody = require('../web/fetch/body.js').extractBody } - const [bodyStream, contentType] = extractBody(body) + const { 0: bodyStream, 1: contentType } = extractBody(body) if (request.contentType == null) { headers.push('content-type', contentType) } diff --git a/lib/web/fetch/body.js b/lib/web/fetch/body.js index 932df3e6532..03557eae85a 100644 --- a/lib/web/fetch/body.js +++ b/lib/web/fetch/body.js @@ -27,6 +27,10 @@ const File = NativeFile ?? UndiciFile const textEncoder = new TextEncoder() const textDecoder = new TextDecoder() +// https://github.com/nodejs/node/issues/44985 +// fix-patch: https://github.com/nodejs/node/pull/51526 +const needReadableStreamTee = util.nodeMajor <= 20 || (util.nodeMajor === 21 && util.nodeMinor <= 6) + // https://fetch.spec.whatwg.org/#concept-bodyinit-extract function extractBody (object, keepalive = false) { // 1. Let stream be null. @@ -125,17 +129,21 @@ function extractBody (object, keepalive = false) { for (const [name, value] of object) { if (typeof value === 'string') { - const chunk = textEncoder.encode(prefix + - `; name="${escape(normalizeLinefeeds(name))}"` + - `\r\n\r\n${normalizeLinefeeds(value)}\r\n`) + const chunk = textEncoder.encode( + `${prefix}; name="${escape( + normalizeLinefeeds(name) + )}"\r\n\r\n${normalizeLinefeeds(value)}\r\n` + ) blobParts.push(chunk) length += chunk.byteLength } else { - const chunk = textEncoder.encode(`${prefix}; name="${escape(normalizeLinefeeds(name))}"` + - (value.name ? `; filename="${escape(value.name)}"` : '') + '\r\n' + - `Content-Type: ${ + const chunk = textEncoder.encode( + `${prefix}; name="${escape(normalizeLinefeeds(name))}"${ + value.name ? `; filename="${escape(value.name)}"` : '' + }\r\nContent-Type: ${ value.type || 'application/octet-stream' - }\r\n\r\n`) + }\r\n\r\n` + ) blobParts.push(chunk, value, rn) if (typeof value.size === 'number') { length += chunk.byteLength + value.size + rn.byteLength @@ -228,7 +236,7 @@ function extractBody (object, keepalive = false) { // bytes into stream. if (!isErrored(stream)) { const buffer = new Uint8Array(value) - if (buffer.byteLength) { + if (buffer.byteLength > 0) { controller.enqueue(buffer) } } @@ -276,16 +284,23 @@ function cloneBody (body) { // 1. Let « out1, out2 » be the result of teeing body’s stream. const [out1, out2] = body.stream.tee() const out2Clone = structuredClone(out2, { transfer: [out2] }) - // This, for whatever reasons, unrefs out2Clone which allows - // the process to exit by itself. - const [, finalClone] = out2Clone.tee() + let streamClone + + if (needReadableStreamTee) { + // This, for whatever reasons, unrefs out2Clone which allows + // the process to exit by itself. + const { 1: finalClone } = out2Clone.tee() + streamClone = finalClone + } else { + streamClone = out2Clone + } // 2. Set body’s stream to out1. body.stream = out1 // 3. Return a body whose stream is out2 and other members are copied from body. return { - stream: finalClone, + stream: streamClone, length: body.length, source: body.source } @@ -435,7 +450,7 @@ function bodyMixinMethods (instance) { // 3. Return a new FormData object whose entries are entries. const formData = new FormData() - for (const [name, value] of entries) { + for (const { 0: name, 1: value } of entries) { formData.append(name, value) } return formData diff --git a/lib/web/fetch/constants.js b/lib/web/fetch/constants.js index ada622feed5..c30c3716644 100644 --- a/lib/web/fetch/constants.js +++ b/lib/web/fetch/constants.js @@ -53,6 +53,7 @@ const requestCache = [ ] // https://fetch.spec.whatwg.org/#request-body-header-name +// Note: The header names are should be lowercase. const requestBodyHeader = [ 'content-encoding', 'content-language', diff --git a/lib/web/fetch/index.js b/lib/web/fetch/index.js index 37e269fbc93..5e2a2f08c24 100644 --- a/lib/web/fetch/index.js +++ b/lib/web/fetch/index.js @@ -1313,8 +1313,8 @@ function httpRedirectFetch (fetchParams, response) { // 2. For each headerName of request-body-header name, delete headerName from // request’s header list. - for (const headerName of requestBodyHeader) { - request.headersList.delete(headerName) + for (let i = 0; i < requestBodyHeader.length; ++i) { + request.headersList.delete(requestBodyHeader[i], true) } } @@ -1475,7 +1475,7 @@ async function httpNetworkOrCacheFetch ( // user agents should append `User-Agent`/default `User-Agent` value to // httpRequest’s header list. if (!httpRequest.headersList.contains('user-agent', true)) { - httpRequest.headersList.append('user-agent', defaultUserAgent) + httpRequest.headersList.append('user-agent', defaultUserAgent, true) } // 15. If httpRequest’s cache mode is "default" and httpRequest’s header @@ -2078,7 +2078,11 @@ async function httpNetworkFetch ( path: url.pathname + url.search, origin: url.origin, method: request.method, - body: agent.isMockActive ? request.body && (request.body.source || request.body.stream) : body, + // https://github.com/nodejs/undici/issues/2418 + body: agent.isMockActive + // FIXME: Why prioritize source? + ? request.body && (request.body.source || request.body.stream) + : body, headers: request.headersList.entries, maxRedirections: 0, upgrade: request.mode === 'websocket' ? 'websocket' : undefined diff --git a/lib/web/fetch/request.js b/lib/web/fetch/request.js index afe92499267..b462c431869 100644 --- a/lib/web/fetch/request.js +++ b/lib/web/fetch/request.js @@ -50,7 +50,7 @@ class Request { webidl.argumentLengthCheck(arguments, 1, { header: 'Request constructor' }) - input = webidl.converters.RequestInfo(input) + input = webidl.converters.RequestInfo_DOMString(input) init = webidl.converters.RequestInit(init) // https://html.spec.whatwg.org/multipage/webappapis.html#environment-settings-object @@ -467,7 +467,8 @@ class Request { // list, append header’s name/header’s value to this’s headers. if (headers instanceof HeadersList) { for (const [key, val] of headers) { - headersList.append(key, val) + // Note: The header names are already in lowercase. + headersList.append(key, val, true) } // Note: Copy the `set-cookie` meta-data. headersList.cookies = headers.cookies @@ -499,7 +500,7 @@ class Request { // 1. Let Content-Type be null. // 2. Set initBody and Content-Type to the result of extracting // init["body"], with keepalive set to request’s keepalive. - const [extractedBody, contentType] = extractBody( + const { 0: extractedBody, 1: contentType } = extractBody( init.body, request.keepalive ) @@ -904,6 +905,19 @@ webidl.converters.RequestInfo = function (V) { return webidl.converters.USVString(V) } +// DOMString is used because the value is converted to a USVString in `new URL()`. +webidl.converters.RequestInfo_DOMString = function (V) { + if (typeof V === 'string') { + return webidl.converters.DOMString(V) + } + + if (V instanceof Request) { + return webidl.converters.Request(V) + } + + return webidl.converters.DOMString(V) +} + webidl.converters.AbortSignal = webidl.interfaceConverter( AbortSignal ) @@ -921,7 +935,7 @@ webidl.converters.RequestInit = webidl.dictionaryConverter([ { key: 'body', converter: webidl.nullableConverter( - webidl.converters.BodyInit + webidl.converters.BodyInit_DOMString ) }, { diff --git a/lib/web/fetch/response.js b/lib/web/fetch/response.js index e31f619590f..caa4e8b68e0 100644 --- a/lib/web/fetch/response.js +++ b/lib/web/fetch/response.js @@ -77,7 +77,8 @@ class Response { webidl.argumentLengthCheck(arguments, 1, { header: 'Response.redirect' }) - url = webidl.converters.USVString(url) + // DOMString is used because the value is converted to a USVString in `new URL()`. + url = webidl.converters.DOMString(url) status = webidl.converters['unsigned short'](status) // 1. Let parsedURL be the result of parsing url with current settings @@ -120,7 +121,7 @@ class Response { } if (body !== null) { - body = webidl.converters.BodyInit(body) + body = webidl.converters.BodyInit_DOMString(body) } init = webidl.converters.ResponseInit(init) @@ -540,6 +541,30 @@ webidl.converters.XMLHttpRequestBodyInit = function (V) { return webidl.converters.DOMString(V) } +webidl.converters.XMLHttpRequestBodyInit_DOMString = function (V) { + if (typeof V === 'string') { + return webidl.converters.DOMtring(V) + } + + if (isBlobLike(V)) { + return webidl.converters.Blob(V, { strict: false }) + } + + if (ArrayBuffer.isView(V) || types.isArrayBuffer(V)) { + return webidl.converters.BufferSource(V) + } + + if (util.isFormDataLike(V)) { + return webidl.converters.FormData(V, { strict: false }) + } + + if (V instanceof URLSearchParams) { + return webidl.converters.URLSearchParams(V) + } + + return webidl.converters.DOMString(V) +} + // https://fetch.spec.whatwg.org/#bodyinit webidl.converters.BodyInit = function (V) { if (V instanceof ReadableStream) { @@ -555,6 +580,21 @@ webidl.converters.BodyInit = function (V) { return webidl.converters.XMLHttpRequestBodyInit(V) } +// https://fetch.spec.whatwg.org/#bodyinit +webidl.converters.BodyInit_DOMString = function (V) { + if (V instanceof ReadableStream) { + return webidl.converters.ReadableStream(V) + } + + // Note: the spec doesn't include async iterables, + // this is an undici extension. + if (V?.[Symbol.asyncIterator]) { + return V + } + + return webidl.converters.XMLHttpRequestBodyInit_DOMString(V) +} + webidl.converters.ResponseInit = webidl.dictionaryConverter([ { key: 'status', diff --git a/lib/web/fetch/util.js b/lib/web/fetch/util.js index 92bcb6cb202..eefe372e785 100644 --- a/lib/web/fetch/util.js +++ b/lib/web/fetch/util.js @@ -1322,8 +1322,9 @@ function extractMimeType (headers) { // 6.4.2. If mimeType’s parameters["charset"] exists, then set charset to // mimeType’s parameters["charset"]. - if (mimeType.parameters.has('charset')) { - charset = mimeType.parameters.get('charset') + const maybeCharset = mimeType.parameters.get('charset') + if (maybeCharset !== undefined) { + charset = maybeCharset } // 6.4.3. Set essence to mimeType’s essence.