Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions src/listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ const responseViaResponseObject = async (

const values: Uint8Array[] = []
let done = false
let readError: { error: unknown } | undefined
let currentReadPromise: Promise<ReadableStreamReadResult<Uint8Array>> | undefined = undefined

if (resHeaderRecord['transfer-encoding'] !== 'chunked') {
Expand All @@ -364,9 +365,11 @@ const responseViaResponseObject = async (
for (let i = 0; i < maxReadCount; i++) {
currentReadPromise ||= reader.read()
const chunk = await readWithoutBlocking(currentReadPromise).catch((e) => {
console.error(e)
done = true
readError = { error: e }
})
if (readError) {
break
}
if (!chunk) {
if (i === 1) {
// XXX: In Node.js v24, some response bodies are not read all the way through until the next task queue,
Expand All @@ -376,9 +379,7 @@ const responseViaResponseObject = async (
continue
}

// Error occurred or currentReadPromise is not yet resolved.
// If an error occurs, immediately break the loop.
// If currentReadPromise is not yet resolved, pass it to writeFromReadableStreamDefaultReader.
// currentReadPromise is not yet resolved, so pass it to writeFromReadableStreamDefaultReader.
break
}
currentReadPromise = undefined
Expand All @@ -392,6 +393,14 @@ const responseViaResponseObject = async (
}
}

if (readError) {
// The body failed before anything was sent, so it must not be reported as a
// complete (possibly empty) response.
handleResponseError(readError.error, outgoing)
;(outgoing as OutgoingHasOutgoingEnded)[outgoingEnded]?.()
return
}

if (done && !('content-length' in resHeaderRecord)) {
resHeaderRecord['content-length'] = values.reduce((acc, value) => acc + value.length, 0)
}
Expand Down
35 changes: 35 additions & 0 deletions test/listener.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,41 @@ describe('Invalid request', () => {
})
})

describe('Response body errors - native Response', () => {
const requestListener = getRequestListener((req) => {
const path = new URL(req.url).pathname
return new GlobalResponse(
new ReadableStream({
start(controller) {
if (path === '/after-chunk') {
controller.enqueue(new TextEncoder().encode('partial'))
}
controller.error(new Error('failed'))
},
})
)
})
const server = createServer(requestListener)

beforeEach(() => {
vi.spyOn(console, 'error').mockImplementation(() => {})
})

afterEach(() => {
vi.restoreAllMocks()
})

it('Should not send a complete empty 200 when the body fails before its first chunk', async () => {
const res = await requestServer(server, { method: 'GET', path: '/' })
expect(res.status).toBe(500)
})

it('Should not send a complete partial 200 when the body fails during the early read', async () => {
const res = await requestServer(server, { method: 'GET', path: '/after-chunk' })
expect(res.status).toBe(500)
})
})

describe('Response headers', () => {
const requestListener = getRequestListener(() => new Response(null), {
hostname: 'example.com',
Expand Down