From 2f13639ba30031d87432f35a6df0276caff8e1df Mon Sep 17 00:00:00 2001 From: zirkelc Date: Thu, 24 Sep 2026 18:38:28 +0200 Subject: [PATCH 1/2] perf(fetch): build the inner request once for string input --- lib/web/fetch/request.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/web/fetch/request.js b/lib/web/fetch/request.js index 66f8150cb82..c9f91623563 100644 --- a/lib/web/fetch/request.js +++ b/lib/web/fetch/request.js @@ -153,7 +153,9 @@ class Request { } // 4. Set request to a new request whose URL is parsedURL. - request = makeRequest({ urlList: [parsedURL] }) + // Note: Step 12 copies this request into a new one and makeRequest fills + // every missing field with its default, so the URL list is all it needs. + request = { urlList: [parsedURL] } // 5. Set fallbackMode to "cors". fallbackMode = 'cors' From 98af98eb24df2183058523e9ccee2d62fec186a3 Mon Sep 17 00:00:00 2001 From: zirkelc Date: Thu, 24 Sep 2026 16:09:22 +0200 Subject: [PATCH 2/2] perf(fetch): skip the header list round trip when RequestInit has no headers --- lib/web/fetch/request.js | 24 +++++++++--------------- test/fetch/request.js | 25 +++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/lib/web/fetch/request.js b/lib/web/fetch/request.js index c9f91623563..191283bc55a 100644 --- a/lib/web/fetch/request.js +++ b/lib/web/fetch/request.js @@ -488,29 +488,23 @@ class Request { } // 32. If init is not empty, then: - if (initHasKey) { - /** @type {HeadersList} */ - const headersList = getHeadersList(this.#headers) + // Note: Without init["headers"], these steps empty the header list and + // append a copy of it back, which leaves it unchanged, so they are skipped. + // init["headers"] is never a Headers object here: the HeadersInit + // converter turns one into a list of its entries, which fill() appends. + if (initHasKey && init.headers !== undefined) { // 1. Let headers be a copy of this’s headers and its associated header // list. // 2. If init["headers"] exists, then set headers to init["headers"]. - const headers = init.headers !== undefined ? init.headers : new HeadersList(headersList) + const headers = init.headers // 3. Empty this’s headers’s header list. - headersList.clear() + getHeadersList(this.#headers).clear() // 4. If headers is a Headers object, then for each header in its header // list, append header’s name/header’s value to this’s headers. - if (headers instanceof HeadersList) { - for (const { name, value } of headers.rawValues()) { - headersList.append(name, value, false) - } - // Note: Copy the `set-cookie` meta-data. - headersList.cookies = headers.cookies - } else { - // 5. Otherwise, fill this’s headers with headers. - fillHeaders(this.#headers, headers) - } + // 5. Otherwise, fill this’s headers with headers. + fillHeaders(this.#headers, headers) } // 33. Let inputBody be input’s request’s body if input is a Request diff --git a/test/fetch/request.js b/test/fetch/request.js index 51e92b5c061..4e47b49a7fd 100644 --- a/test/fetch/request.js +++ b/test/fetch/request.js @@ -431,6 +431,31 @@ test('Clone the set-cookie header when Request is passed as the first parameter t.assert.strictEqual(request2.headers.getSetCookie().join(', '), request2.headers.get('set-cookie')) }) +test('Request with a Request input and an init without headers keeps a copy of the headers', (t) => { + const request = new Request('http://localhost', { + headers: [['Set-Cookie', 'a=1'], ['X-B', '2'], ['set-cookie', 'c=3'], ['x-a', '1']] + }) + const request2 = new Request(request, { method: 'PUT' }) + + t.assert.strictEqual(request2.method, 'PUT') + t.assert.deepStrictEqual([...request2.headers], [...request.headers]) + t.assert.deepStrictEqual(request2.headers.getSetCookie(), ['a=1', 'c=3']) + + request2.headers.append('set-cookie', 'd=4') + request.headers.append('x-c', '3') + t.assert.deepStrictEqual(request.headers.getSetCookie(), ['a=1', 'c=3']) + t.assert.deepStrictEqual(request2.headers.getSetCookie(), ['a=1', 'c=3', 'd=4']) + t.assert.strictEqual(request2.headers.get('x-c'), null) +}) + +test('Request with a Headers object in init keeps all of its entries', (t) => { + const headers = new Headers([['X-A', '1'], ['set-cookie', 'a=1'], ['set-cookie', 'b=2']]) + const request = new Request('http://localhost', { headers: { 'x-old': '0' } }) + const request2 = new Request(request, { headers }) + + t.assert.deepStrictEqual([...request2.headers], [['set-cookie', 'a=1'], ['set-cookie', 'b=2'], ['x-a', '1']]) +}) + // Tests for optimization introduced in https://github.com/nodejs/undici/pull/2456 test('keys to object prototypes method', (t) => { const request = new Request('http://localhost', { method: 'hasOwnProperty' })