Skip to content

Commit c944899

Browse files
authored
fix: do not overwrite Content-Length in the fast path pattern if Content-Length already exists. (#309)
* fix: do not overwrite Content-Length in the fast path pattern if Content-Length already exists. * fix: support tuple headers in responseViaCache
1 parent 2f8ca36 commit c944899

3 files changed

Lines changed: 94 additions & 12 deletions

File tree

src/listener.ts

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,17 +70,35 @@ const responseViaCache = async (
7070
): Promise<undefined | void> => {
7171
// eslint-disable-next-line @typescript-eslint/no-explicit-any
7272
let [status, body, header] = (res as any)[cacheKey] as InternalCache
73-
if (header instanceof Headers) {
73+
74+
let hasContentLength = false
75+
if (!header) {
76+
header = { 'content-type': 'text/plain; charset=UTF-8' }
77+
} else if (header instanceof Headers) {
78+
hasContentLength = header.has('content-length')
7479
header = buildOutgoingHttpHeaders(header)
80+
} else if (Array.isArray(header)) {
81+
const headerObj = new Headers(header)
82+
hasContentLength = headerObj.has('content-length')
83+
header = buildOutgoingHttpHeaders(headerObj)
84+
} else {
85+
for (const key in header) {
86+
if (key.length === 14 && key.toLowerCase() === 'content-length') {
87+
hasContentLength = true
88+
break
89+
}
90+
}
7591
}
7692

7793
// in `responseViaCache`, if body is not stream, Transfer-Encoding is considered not chunked
78-
if (typeof body === 'string') {
79-
header['Content-Length'] = Buffer.byteLength(body)
80-
} else if (body instanceof Uint8Array) {
81-
header['Content-Length'] = body.byteLength
82-
} else if (body instanceof Blob) {
83-
header['Content-Length'] = body.size
94+
if (!hasContentLength) {
95+
if (typeof body === 'string') {
96+
header['Content-Length'] = Buffer.byteLength(body)
97+
} else if (body instanceof Uint8Array) {
98+
header['Content-Length'] = body.byteLength
99+
} else if (body instanceof Blob) {
100+
header['Content-Length'] = body.size
101+
}
84102
}
85103

86104
outgoing.writeHead(status, header)

src/response.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const cacheKey = Symbol('cache')
1010
export type InternalCache = [
1111
number,
1212
string | ReadableStream,
13-
Record<string, string> | Headers | OutgoingHttpHeaders,
13+
Record<string, string> | [string, string][] | Headers | OutgoingHttpHeaders | undefined,
1414
]
1515
interface LightResponse {
1616
[responseCache]?: globalThis.Response
@@ -28,7 +28,7 @@ export class Response {
2828
}
2929

3030
constructor(body?: BodyInit | null, init?: ResponseInit) {
31-
let headers: HeadersInit
31+
let headers: HeadersInit | undefined
3232
this.#body = body
3333
if (init instanceof Response) {
3434
const cachedGlobalResponse = (init as any)[responseCache]
@@ -52,16 +52,17 @@ export class Response {
5252
body instanceof Blob ||
5353
body instanceof Uint8Array
5454
) {
55-
headers ||= init?.headers || { 'content-type': 'text/plain; charset=UTF-8' }
56-
;(this as any)[cacheKey] = [init?.status || 200, body, headers]
55+
;(this as any)[cacheKey] = [init?.status || 200, body, headers || init?.headers]
5756
}
5857
}
5958

6059
get headers(): Headers {
6160
const cache = (this as LightResponse)[cacheKey] as InternalCache
6261
if (cache) {
6362
if (!(cache[2] instanceof Headers)) {
64-
cache[2] = new Headers(cache[2] as HeadersInit)
63+
cache[2] = new Headers(
64+
(cache[2] || { 'content-type': 'text/plain; charset=UTF-8' }) as HeadersInit
65+
)
6566
}
6667
return cache[2]
6768
}

test/server.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,37 @@ describe('various response body types', () => {
242242
})
243243
return response
244244
})
245+
app.get('/text-with-content-length-object', () => {
246+
const response = new Response('Hello Hono!', {
247+
headers: { 'content-type': 'text/plain', 'content-length': '00011' },
248+
})
249+
return response
250+
})
251+
app.get('/text-with-content-length-headers', () => {
252+
const response = new Response('Hello Hono!', {
253+
headers: new Headers({ 'content-type': 'text/plain', 'content-length': '00011' }),
254+
})
255+
return response
256+
})
257+
app.get('/text-with-content-length-array', () => {
258+
const response = new Response('Hello Hono!', {
259+
headers: [
260+
['content-type', 'text/plain'],
261+
['content-length', '00011'],
262+
],
263+
})
264+
return response
265+
})
266+
app.get('/text-with-set-cookie-array', () => {
267+
const response = new Response('Hello Hono!', {
268+
headers: [
269+
['content-type', 'text/plain'],
270+
['set-cookie', 'a=1'],
271+
['set-cookie', 'b=2'],
272+
],
273+
})
274+
return response
275+
})
245276

246277
app.use('/etag/*', etag())
247278
app.get('/etag/buffer', () => {
@@ -372,6 +403,38 @@ describe('various response body types', () => {
372403
expect(res.text).toBe('Hello Hono!')
373404
})
374405

406+
it('Should return 200 response - GET /text-with-content-length-object', async () => {
407+
const res = await request(server).get('/text-with-content-length-object')
408+
expect(res.status).toBe(200)
409+
expect(res.headers['content-type']).toMatch('text/plain')
410+
expect(res.headers['content-length']).toBe('00011')
411+
expect(res.text).toBe('Hello Hono!')
412+
})
413+
414+
it('Should return 200 response - GET /text-with-content-length-headers', async () => {
415+
const res = await request(server).get('/text-with-content-length-headers')
416+
expect(res.status).toBe(200)
417+
expect(res.headers['content-type']).toMatch('text/plain')
418+
expect(res.headers['content-length']).toBe('00011')
419+
expect(res.text).toBe('Hello Hono!')
420+
})
421+
422+
it('Should return 200 response - GET /text-with-content-length-array', async () => {
423+
const res = await request(server).get('/text-with-content-length-array')
424+
expect(res.status).toBe(200)
425+
expect(res.headers['content-type']).toMatch('text/plain')
426+
expect(res.headers['content-length']).toBe('00011')
427+
expect(res.text).toBe('Hello Hono!')
428+
})
429+
430+
it('Should return 200 response - GET /text-with-set-cookie-array', async () => {
431+
const res = await request(server).get('/text-with-set-cookie-array')
432+
expect(res.status).toBe(200)
433+
expect(res.headers['content-type']).toMatch('text/plain')
434+
expect(res.headers['set-cookie']).toEqual(['a=1', 'b=2'])
435+
expect(res.text).toBe('Hello Hono!')
436+
})
437+
375438
it('Should return 200 response - GET /etag/buffer', async () => {
376439
const res = await request(server).get('/etag/buffer')
377440
expect(res.status).toBe(200)

0 commit comments

Comments
 (0)