From 3e577bca889b866d6efa29f308b0bb1c50a7cd82 Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Tue, 19 May 2026 12:06:24 +0100 Subject: [PATCH 1/2] Make headers independent --- .../fixtures/app/src/request-clone.js | 18 ++++++++++++++++++ .../js-compute/fixtures/app/tests.json | 1 + .../fastly/builtins/fetch/request-response.cpp | 9 ++++++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/integration-tests/js-compute/fixtures/app/src/request-clone.js b/integration-tests/js-compute/fixtures/app/src/request-clone.js index 1cbc2c3a9e..bd0067efaa 100644 --- a/integration-tests/js-compute/fixtures/app/src/request-clone.js +++ b/integration-tests/js-compute/fixtures/app/src/request-clone.js @@ -46,6 +46,24 @@ routes.set('/request/clone/valid', async () => { assert(newRequest.bodyUsed, false, 'newRequest.bodyUsed'); assert(newRequest.body, null, 'newRequest.body'); }); +routes.set('/request/clone/headers-are-independent', () => { + const request = new Request('https://www.fastly.com', { + headers: { 'x-foo': 'original' }, + method: 'get', + }); + const cloned = request.clone(); + + // Mutating the clone's headers must not affect the original + cloned.headers.set('x-foo', 'mutated'); + assert(request.headers.get('x-foo'), 'original', 'original header unchanged after mutating clone'); + + // Mutating the original's headers must not affect the clone + request.headers.set('x-bar', 'added'); + assert(cloned.headers.get('x-bar'), null, 'clone does not see header added to original'); + + // Clone must carry the headers that existed at clone time + assert(cloned.headers.get('x-foo'), 'mutated', 'clone has header value set on it'); +}); routes.set('/request/clone/invalid', async () => { const request = new Request('https://www.fastly.com', { headers: { diff --git a/integration-tests/js-compute/fixtures/app/tests.json b/integration-tests/js-compute/fixtures/app/tests.json index 6817434bf3..b46c16f05f 100644 --- a/integration-tests/js-compute/fixtures/app/tests.json +++ b/integration-tests/js-compute/fixtures/app/tests.json @@ -1222,6 +1222,7 @@ "GET /request/clone/called-as-constructor": {}, "GET /request/clone/called-unbound": {}, "GET /request/clone/valid": {}, + "GET /request/clone/headers-are-independent": {}, "GET /request/clone/invalid": {}, "POST /request/body-async-simple/no-workaround": { "environments": ["viceroy", "compute"], diff --git a/runtime/fastly/builtins/fetch/request-response.cpp b/runtime/fastly/builtins/fetch/request-response.cpp index 7dee8a0338..e4ad9dd189 100644 --- a/runtime/fastly/builtins/fetch/request-response.cpp +++ b/runtime/fastly/builtins/fetch/request-response.cpp @@ -2450,8 +2450,15 @@ bool Request::clone(JSContext *cx, unsigned argc, JS::Value *vp) { return false; } + JS::RootedValue headers_val(cx, JS::ObjectValue(*headers)); + JS::RootedObject cloned_headers( + cx, Headers::create(cx, headers_val, Headers::guard(headers))); + if (!cloned_headers) { + return false; + } + JS::SetReservedSlot(requestInstance, static_cast(Slots::Headers), - JS::ObjectValue(*headers)); + JS::ObjectValue(*cloned_headers)); JS::RootedString method(cx, Request::method(cx, self)); if (!method) { From fc578cc8d8fd5b84e5fed04f6cbd3ae1c94ec36e Mon Sep 17 00:00:00 2001 From: Sy Brand Date: Tue, 19 May 2026 12:10:04 +0100 Subject: [PATCH 2/2] fmt --- .../fixtures/app/src/request-clone.js | 18 +++++++++++++++--- .../fastly/builtins/fetch/request-response.cpp | 9 +-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/integration-tests/js-compute/fixtures/app/src/request-clone.js b/integration-tests/js-compute/fixtures/app/src/request-clone.js index bd0067efaa..982ecf249d 100644 --- a/integration-tests/js-compute/fixtures/app/src/request-clone.js +++ b/integration-tests/js-compute/fixtures/app/src/request-clone.js @@ -55,14 +55,26 @@ routes.set('/request/clone/headers-are-independent', () => { // Mutating the clone's headers must not affect the original cloned.headers.set('x-foo', 'mutated'); - assert(request.headers.get('x-foo'), 'original', 'original header unchanged after mutating clone'); + assert( + request.headers.get('x-foo'), + 'original', + 'original header unchanged after mutating clone', + ); // Mutating the original's headers must not affect the clone request.headers.set('x-bar', 'added'); - assert(cloned.headers.get('x-bar'), null, 'clone does not see header added to original'); + assert( + cloned.headers.get('x-bar'), + null, + 'clone does not see header added to original', + ); // Clone must carry the headers that existed at clone time - assert(cloned.headers.get('x-foo'), 'mutated', 'clone has header value set on it'); + assert( + cloned.headers.get('x-foo'), + 'mutated', + 'clone has header value set on it', + ); }); routes.set('/request/clone/invalid', async () => { const request = new Request('https://www.fastly.com', { diff --git a/runtime/fastly/builtins/fetch/request-response.cpp b/runtime/fastly/builtins/fetch/request-response.cpp index e4ad9dd189..7dee8a0338 100644 --- a/runtime/fastly/builtins/fetch/request-response.cpp +++ b/runtime/fastly/builtins/fetch/request-response.cpp @@ -2450,15 +2450,8 @@ bool Request::clone(JSContext *cx, unsigned argc, JS::Value *vp) { return false; } - JS::RootedValue headers_val(cx, JS::ObjectValue(*headers)); - JS::RootedObject cloned_headers( - cx, Headers::create(cx, headers_val, Headers::guard(headers))); - if (!cloned_headers) { - return false; - } - JS::SetReservedSlot(requestInstance, static_cast(Slots::Headers), - JS::ObjectValue(*cloned_headers)); + JS::ObjectValue(*headers)); JS::RootedString method(cx, Request::method(cx, self)); if (!method) {