Skip to content

Commit 6ff147a

Browse files
committed
fix: preserve status and statusText when cloning a Response with live headers
The lightweight Response built its GlobalResponse via `{ ...this.#init }`, but `#init` may hold a Response instance whose `status`/`statusText` are prototype getters and are lost when spread. Read those fields explicitly so they survive alongside the live headers.
1 parent 44c365a commit 6ff147a

2 files changed

Lines changed: 42 additions & 14 deletions

File tree

src/response.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,13 @@ export class Response {
3333
delete (this as LightResponse)[cacheKey]
3434
return ((this as LightResponse)[responseCache] ||= new GlobalResponse(
3535
this.#body,
36-
liveHeaders ? { ...this.#init, headers: liveHeaders } : this.#init
36+
liveHeaders
37+
? {
38+
status: this.#init?.status,
39+
statusText: this.#init?.statusText,
40+
headers: liveHeaders,
41+
}
42+
: this.#init
3743
))
3844
}
3945

test/response.test.ts

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -89,32 +89,54 @@ describe('Response', () => {
8989
expect(childResponse.headers.get('content-type')).toEqual('application/json')
9090
})
9191

92-
it('Should preserve headers mutated after construction when cloned via new Response(body, init)', () => {
93-
// Regression test for https://github.com/honojs/node-server/issues/304.
94-
// Headers appended (or set/deleted) after construction must be visible on
95-
// a clone built with `new Response(body, parent)` — which is the pattern
96-
// used by middleware such as `cors` and `compress`.
92+
it('Should preserve mutated headers when cloned before body access', () => {
9793
const parentResponse = new Response('hello', {
9894
status: 200,
9995
headers: { 'content-type': 'application/json' },
10096
})
10197
parentResponse.headers.append('set-cookie', 'session=abc; Path=/; HttpOnly')
10298

103-
// Pattern 1: clone before any `getResponseCache`-triggering access.
10499
const childResponse = new Response('hello', parentResponse)
105100
expect(childResponse.headers.get('set-cookie')).toEqual('session=abc; Path=/; HttpOnly')
106101
expect(childResponse.headers.get('content-type')).toEqual('application/json')
102+
})
107103

108-
// Pattern 2: clone after `.body` has materialized the GlobalResponse —
109-
// this is what middleware does when streaming a raw Response body.
110-
const parentForBody = new Response('hello', {
104+
it('Should preserve mutated headers when cloned after body access', () => {
105+
const parentResponse = new Response('hello', {
111106
status: 200,
112107
headers: { 'content-type': 'application/json' },
113108
})
114-
parentForBody.headers.append('set-cookie', 'session=xyz; Path=/; HttpOnly')
115-
const streamedChild = new Response(parentForBody.body, parentForBody)
116-
expect(streamedChild.headers.get('set-cookie')).toEqual('session=xyz; Path=/; HttpOnly')
117-
expect(streamedChild.headers.get('content-type')).toEqual('application/json')
109+
parentResponse.headers.append('set-cookie', 'session=xyz; Path=/; HttpOnly')
110+
111+
const childResponse = new Response(parentResponse.body, parentResponse)
112+
expect(childResponse.headers.get('set-cookie')).toEqual('session=xyz; Path=/; HttpOnly')
113+
expect(childResponse.headers.get('content-type')).toEqual('application/json')
114+
})
115+
116+
it('Should preserve status and statusText when headers are mutated', () => {
117+
const res = new Response('hello', {
118+
status: 201,
119+
statusText: 'Created',
120+
headers: { 'content-type': 'application/json' },
121+
})
122+
res.headers.append('set-cookie', 'session=abc; Path=/; HttpOnly')
123+
124+
expect(res.status).toEqual(201)
125+
expect(res.statusText).toEqual('Created')
126+
expect(res.headers.get('set-cookie')).toEqual('session=abc; Path=/; HttpOnly')
127+
})
128+
129+
it('Should preserve status from a native Response after materialization', () => {
130+
const nativeRedirect = new GlobalResponse(null, {
131+
status: 302,
132+
headers: { location: 'https://example.com/' },
133+
})
134+
const res = new Response(nativeRedirect.body, nativeRedirect)
135+
136+
expect(res.status).toEqual(302)
137+
void res.body
138+
expect(res.status).toEqual(302)
139+
expect(res.headers.get('location')).toEqual('https://example.com/')
118140
})
119141

120142
it('Nested constructors should not cause an error even if ReadableStream is specified', async () => {

0 commit comments

Comments
 (0)