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..92ed53885dd 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.USVString(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.