Skip to content

Commit 16d1029

Browse files
committed
fix(listener): skip adding content-length header when transfer-encoding is chunked
1 parent 98f2792 commit 16d1029

File tree

2 files changed

+57
-35
lines changed

2 files changed

+57
-35
lines changed

src/listener.ts

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,14 @@ const responseViaCache = async (
7474
header = buildOutgoingHttpHeaders(header)
7575
}
7676

77-
if (typeof body === 'string') {
78-
header['Content-Length'] = Buffer.byteLength(body)
79-
} else if (body instanceof Uint8Array) {
80-
header['Content-Length'] = body.byteLength
81-
} else if (body instanceof Blob) {
82-
header['Content-Length'] = body.size
77+
if (header['transfer-encoding'] !== '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
84+
}
8385
}
8486

8587
outgoing.writeHead(status, header)
@@ -131,42 +133,44 @@ const responseViaResponseObject = async (
131133
let done = false
132134
let currentReadPromise: Promise<ReadableStreamReadResult<Uint8Array>> | undefined = undefined
133135

134-
// In the case of synchronous responses, usually a maximum of two readings is done
135-
for (let i = 0; i < 2; i++) {
136-
currentReadPromise = reader.read()
137-
const chunk = await readWithoutBlocking(currentReadPromise).catch((e) => {
138-
console.error(e)
139-
done = true
140-
})
141-
if (!chunk) {
142-
if (i === 1 && resHeaderRecord['transfer-encoding'] !== 'chunked') {
143-
// XXX: In Node.js v24, some response bodies are not read all the way through until the next task queue,
144-
// so wait a moment and retry. (e.g. new Blob([new Uint8Array(contents)]) )
145-
await new Promise((resolve) => setTimeout(resolve))
146-
i--
147-
continue
136+
if (resHeaderRecord['transfer-encoding'] !== 'chunked') {
137+
// In the case of synchronous responses, usually a maximum of two readings is done
138+
for (let i = 0; i < 2; i++) {
139+
currentReadPromise = reader.read()
140+
const chunk = await readWithoutBlocking(currentReadPromise).catch((e) => {
141+
console.error(e)
142+
done = true
143+
})
144+
if (!chunk) {
145+
if (i === 1) {
146+
// XXX: In Node.js v24, some response bodies are not read all the way through until the next task queue,
147+
// so wait a moment and retry. (e.g. new Blob([new Uint8Array(contents)]) )
148+
await new Promise((resolve) => setTimeout(resolve))
149+
i--
150+
continue
151+
}
152+
153+
// Error occurred or currentReadPromise is not yet resolved.
154+
// If an error occurs, immediately break the loop.
155+
// If currentReadPromise is not yet resolved, pass it to writeFromReadableStreamDefaultReader.
156+
break
148157
}
158+
currentReadPromise = undefined
149159

150-
// Error occurred or currentReadPromise is not yet resolved.
151-
// If an error occurs, immediately break the loop.
152-
// If currentReadPromise is not yet resolved, pass it to writeFromReadableStreamDefaultReader.
153-
break
160+
if (chunk.value) {
161+
values.push(chunk.value)
162+
}
163+
if (chunk.done) {
164+
done = true
165+
break
166+
}
154167
}
155-
currentReadPromise = undefined
156168

157-
if (chunk.value) {
158-
values.push(chunk.value)
159-
}
160-
if (chunk.done) {
161-
done = true
162-
break
169+
if (done && !('content-length' in resHeaderRecord)) {
170+
resHeaderRecord['content-length'] = values.reduce((acc, value) => acc + value.length, 0)
163171
}
164172
}
165173

166-
if (done && !('content-length' in resHeaderRecord)) {
167-
resHeaderRecord['content-length'] = values.reduce((acc, value) => acc + value.length, 0)
168-
}
169-
170174
outgoing.writeHead(res.status, resHeaderRecord)
171175
values.forEach((value) => {
172176
;(outgoing as Writable).write(value)

test/server.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,15 @@ describe('various response body types', () => {
169169
})
170170
return response
171171
})
172+
app.get('/blob-chunked', () => {
173+
const response = new Response(new Blob([new Uint8Array([1, 2, 3])]), {
174+
headers: {
175+
'transfer-encoding': 'chunked',
176+
'content-type': 'application/octet-stream',
177+
},
178+
})
179+
return response
180+
})
172181
const readableStreamPromise = new Promise<void>((resolve) => {
173182
resolveReadableStreamPromise = resolve
174183
})
@@ -233,6 +242,15 @@ describe('various response body types', () => {
233242
expect(res.body).toEqual(Buffer.from([1, 2, 3]))
234243
})
235244

245+
it('Should return 200 response - GET /blob-chunked', async () => {
246+
const res = await request(server).get('/blob-chunked')
247+
expect(res.status).toBe(200)
248+
expect(res.headers['transfer-encoding']).toMatch('chunked')
249+
expect(res.headers['content-type']).toMatch('application/octet-stream')
250+
expect(res.headers['content-length']).toBeUndefined()
251+
expect(res.body).toEqual(Buffer.from([1, 2, 3]))
252+
})
253+
236254
it('Should return 200 response - GET /readable-stream', async () => {
237255
const expectedChunks = ['Hello!', ' Node!']
238256
const resPromise = request(server)

0 commit comments

Comments
 (0)