diff --git a/package.json b/package.json index 8c9b064..92c4508 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hono/node-server", - "version": "1.19.10", + "version": "1.19.11", "description": "Node.js Adapter for Hono", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/listener.ts b/src/listener.ts index a9ea3dd..83603bf 100644 --- a/src/listener.ts +++ b/src/listener.ts @@ -70,17 +70,35 @@ const responseViaCache = async ( ): Promise => { // eslint-disable-next-line @typescript-eslint/no-explicit-any let [status, body, header] = (res as any)[cacheKey] as InternalCache - if (header instanceof Headers) { + + let hasContentLength = false + if (!header) { + header = { 'content-type': 'text/plain; charset=UTF-8' } + } else if (header instanceof Headers) { + hasContentLength = header.has('content-length') header = buildOutgoingHttpHeaders(header) + } else if (Array.isArray(header)) { + const headerObj = new Headers(header) + hasContentLength = headerObj.has('content-length') + header = buildOutgoingHttpHeaders(headerObj) + } else { + for (const key in header) { + if (key.length === 14 && key.toLowerCase() === 'content-length') { + hasContentLength = true + break + } + } } // in `responseViaCache`, if body is not stream, Transfer-Encoding is considered not chunked - if (typeof body === 'string') { - header['Content-Length'] = Buffer.byteLength(body) - } else if (body instanceof Uint8Array) { - header['Content-Length'] = body.byteLength - } else if (body instanceof Blob) { - header['Content-Length'] = body.size + if (!hasContentLength) { + if (typeof body === 'string') { + header['Content-Length'] = Buffer.byteLength(body) + } else if (body instanceof Uint8Array) { + header['Content-Length'] = body.byteLength + } else if (body instanceof Blob) { + header['Content-Length'] = body.size + } } outgoing.writeHead(status, header) diff --git a/src/response.ts b/src/response.ts index 914f05f..ae9a59a 100644 --- a/src/response.ts +++ b/src/response.ts @@ -10,7 +10,7 @@ export const cacheKey = Symbol('cache') export type InternalCache = [ number, string | ReadableStream, - Record | Headers | OutgoingHttpHeaders, + Record | [string, string][] | Headers | OutgoingHttpHeaders | undefined, ] interface LightResponse { [responseCache]?: globalThis.Response @@ -28,7 +28,7 @@ export class Response { } constructor(body?: BodyInit | null, init?: ResponseInit) { - let headers: HeadersInit + let headers: HeadersInit | undefined this.#body = body if (init instanceof Response) { const cachedGlobalResponse = (init as any)[responseCache] @@ -52,8 +52,7 @@ export class Response { body instanceof Blob || body instanceof Uint8Array ) { - headers ||= init?.headers || { 'content-type': 'text/plain; charset=UTF-8' } - ;(this as any)[cacheKey] = [init?.status || 200, body, headers] + ;(this as any)[cacheKey] = [init?.status || 200, body, headers || init?.headers] } } @@ -61,7 +60,9 @@ export class Response { const cache = (this as LightResponse)[cacheKey] as InternalCache if (cache) { if (!(cache[2] instanceof Headers)) { - cache[2] = new Headers(cache[2] as HeadersInit) + cache[2] = new Headers( + (cache[2] || { 'content-type': 'text/plain; charset=UTF-8' }) as HeadersInit + ) } return cache[2] } diff --git a/test/server.test.ts b/test/server.test.ts index 40456c7..298eeae 100644 --- a/test/server.test.ts +++ b/test/server.test.ts @@ -242,6 +242,37 @@ describe('various response body types', () => { }) return response }) + app.get('/text-with-content-length-object', () => { + const response = new Response('Hello Hono!', { + headers: { 'content-type': 'text/plain', 'content-length': '00011' }, + }) + return response + }) + app.get('/text-with-content-length-headers', () => { + const response = new Response('Hello Hono!', { + headers: new Headers({ 'content-type': 'text/plain', 'content-length': '00011' }), + }) + return response + }) + app.get('/text-with-content-length-array', () => { + const response = new Response('Hello Hono!', { + headers: [ + ['content-type', 'text/plain'], + ['content-length', '00011'], + ], + }) + return response + }) + app.get('/text-with-set-cookie-array', () => { + const response = new Response('Hello Hono!', { + headers: [ + ['content-type', 'text/plain'], + ['set-cookie', 'a=1'], + ['set-cookie', 'b=2'], + ], + }) + return response + }) app.use('/etag/*', etag()) app.get('/etag/buffer', () => { @@ -372,6 +403,38 @@ describe('various response body types', () => { expect(res.text).toBe('Hello Hono!') }) + it('Should return 200 response - GET /text-with-content-length-object', async () => { + const res = await request(server).get('/text-with-content-length-object') + expect(res.status).toBe(200) + expect(res.headers['content-type']).toMatch('text/plain') + expect(res.headers['content-length']).toBe('00011') + expect(res.text).toBe('Hello Hono!') + }) + + it('Should return 200 response - GET /text-with-content-length-headers', async () => { + const res = await request(server).get('/text-with-content-length-headers') + expect(res.status).toBe(200) + expect(res.headers['content-type']).toMatch('text/plain') + expect(res.headers['content-length']).toBe('00011') + expect(res.text).toBe('Hello Hono!') + }) + + it('Should return 200 response - GET /text-with-content-length-array', async () => { + const res = await request(server).get('/text-with-content-length-array') + expect(res.status).toBe(200) + expect(res.headers['content-type']).toMatch('text/plain') + expect(res.headers['content-length']).toBe('00011') + expect(res.text).toBe('Hello Hono!') + }) + + it('Should return 200 response - GET /text-with-set-cookie-array', async () => { + const res = await request(server).get('/text-with-set-cookie-array') + expect(res.status).toBe(200) + expect(res.headers['content-type']).toMatch('text/plain') + expect(res.headers['set-cookie']).toEqual(['a=1', 'b=2']) + expect(res.text).toBe('Hello Hono!') + }) + it('Should return 200 response - GET /etag/buffer', async () => { const res = await request(server).get('/etag/buffer') expect(res.status).toBe(200)