Repository: nodejs/undici

Problem statement:
undici.Request.clone does not copy dispatcher
## Bug Description

<!-- A clear and concise description of what the bug is. -->

## Reproducible By

Consider this code:
```
async function test() {
    const undici = require('undici');

    class CustomAgent extends undici.Agent {
        constructor() {
            super({
                keepAliveTimeout: 10,
                keepAliveMaxTimeout: 10
            });
        }
        dispatch(options, handler) {
            console.log("DISPATCH");
            return super.dispatch(options, handler);
        }
    }

    const request = new undici.Request("https://example.org", {
        dispatcher: new CustomAgent()
    });

    // will make an HTTP call and print "DISPATCH"
    await undici.fetch(request);

    // will make an HTTP call, but not print "DISPATCH"
    await undici.fetch(request.clone());
}

test();
```

## Expected Behavior

In the code above, I would expect both `fetch` calls to use the dispatcher.

## Logs & Screenshots

none.

## Environment

NodeJS, Version v22.9.0

### Additional context



Hints:

Patch:
diff --git a/lib/web/cache/cache.js b/lib/web/cache/cache.js
--- a/lib/web/cache/cache.js
+++ b/lib/web/cache/cache.js
@@ -514,6 +514,7 @@ class Cache {
       for (const request of requests) {
         const requestObject = fromInnerRequest(
           request,
+          undefined,
           new AbortController().signal,
           'immutable'
         )
diff --git a/lib/web/fetch/request.js b/lib/web/fetch/request.js
--- a/lib/web/fetch/request.js
+++ b/lib/web/fetch/request.js
@@ -775,7 +775,7 @@ class Request {
     }
 
     // 4. Return clonedRequestObject.
-    return fromInnerRequest(clonedRequest, ac.signal, getHeadersGuard(this[kHeaders]))
+    return fromInnerRequest(clonedRequest, this[kDispatcher], ac.signal, getHeadersGuard(this[kHeaders]))
   }
 
   [nodeUtil.inspect.custom] (depth, options) {
@@ -875,13 +875,15 @@ function cloneRequest (request) {
 /**
  * @see https://fetch.spec.whatwg.org/#request-create
  * @param {any} innerRequest
+ * @param {import('../../dispatcher/agent')} dispatcher
  * @param {AbortSignal} signal
  * @param {'request' | 'immutable' | 'request-no-cors' | 'response' | 'none'} guard
  * @returns {Request}
  */
-function fromInnerRequest (innerRequest, signal, guard) {
+function fromInnerRequest (innerRequest, dispatcher, signal, guard) {
   const request = new Request(kConstruct)
   request[kState] = innerRequest
+  request[kDispatcher] = dispatcher
   request[kSignal] = signal
   request[kHeaders] = new Headers(kConstruct)
   setHeadersList(request[kHeaders], innerRequest.headersList)


Test patch:
diff --git a/test/fetch/issue-3630.js b/test/fetch/issue-3630.js
new file mode 100644
--- /dev/null
+++ b/test/fetch/issue-3630.js
@@ -0,0 +1,12 @@
+'use strict'
+
+const { test } = require('node:test')
+const assert = require('node:assert')
+const { Request, Agent } = require('../..')
+const { kDispatcher } = require('../../lib/web/fetch/symbols')
+
+test('Cloned request should inherit its dispatcher', () => {
+  const agent = new Agent()
+  const request = new Request('https://a', { dispatcher: agent })
+  assert.strictEqual(request[kDispatcher], agent)
+})
diff --git a/test/fetch/request.js b/test/fetch/request.js
--- a/test/fetch/request.js
+++ b/test/fetch/request.js
@@ -10,9 +10,6 @@ const {
   Headers,
   fetch
 } = require('../../')
-const { fromInnerRequest, makeRequest } = require('../../lib/web/fetch/request')
-const { kState, kSignal, kHeaders } = require('../../lib/web/fetch/symbols')
-const { getHeadersGuard, getHeadersList } = require('../../lib/web/fetch/headers')
 
 const hasSignalReason = 'reason' in AbortSignal.prototype
 
@@ -462,17 +459,3 @@ test('Issue#2465', async (t) => {
   const request = new Request('http://localhost', { body: new SharedArrayBuffer(0), method: 'POST' })
   strictEqual(await request.text(), '[object SharedArrayBuffer]')
 })
-
-test('fromInnerRequest', () => {
-  const innerRequest = makeRequest({
-    urlList: [new URL('http://asd')]
-  })
-  const signal = new AbortController().signal
-  const request = fromInnerRequest(innerRequest, signal, 'immutable')
-
-  // check property
-  assert.strictEqual(request[kState], innerRequest)
-  assert.strictEqual(request[kSignal], signal)
-  assert.strictEqual(getHeadersList(request[kHeaders]), innerRequest.headersList)
-  assert.strictEqual(getHeadersGuard(request[kHeaders]), 'immutable')
-})