From 97e9f01ff611f9d21c6ab80e59e967753900fb8d Mon Sep 17 00:00:00 2001 From: SUVAJIT-KARMAKAR Date: Fri, 3 Apr 2026 11:27:30 +0530 Subject: [PATCH 1/2] fix: move res.send() outside streaming loop --- packages/better-call/src/adapters/node/request.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/better-call/src/adapters/node/request.ts b/packages/better-call/src/adapters/node/request.ts index ea3edb5..5313198 100644 --- a/packages/better-call/src/adapters/node/request.ts +++ b/packages/better-call/src/adapters/node/request.ts @@ -330,8 +330,8 @@ export async function setResponse(res: ServerResponse, response: Response) { return; } } - res.end(); } + res.end(); } catch (error) { cancel(error instanceof Error ? error : new Error(String(error))); } From 90274b07d43db1acf9c7ce217a87003bb4df84c6 Mon Sep 17 00:00:00 2001 From: SUVAJIT-KARMAKAR Date: Fri, 3 Apr 2026 16:09:47 +0530 Subject: [PATCH 2/2] test: add unit-test for set-response-streaming truncation fix --- .../src/adapters/node/request.test.ts | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/packages/better-call/src/adapters/node/request.test.ts b/packages/better-call/src/adapters/node/request.test.ts index 2fd8571..e3393cb 100644 --- a/packages/better-call/src/adapters/node/request.test.ts +++ b/packages/better-call/src/adapters/node/request.test.ts @@ -416,4 +416,58 @@ describe("setResponse", () => { expect(statusCodeBeforeWriteHead).toBe(400); expect(res.statusCode).toBe(400); }); + + it("should call res.end() after all streamed chunks are written", async () => { + // Regression test for streaming response truncation issue https://github.com/better-auth/better-call/issues/123 + // Previously res.end() was inside the streaming for-loop's inner block, + // which could cause it to be skipped when backpressure triggered an + // early return to wait for the "drain" event, truncating the response. + const socket = new Socket(); + const req = new IncomingMessage(socket); + const res = new ServerResponse(req); + + const callOrder: string[] = []; + + // setResponse calls next() without awaiting it, so we need to + // track when res.end() is called to know streaming is complete. + const endPromise = new Promise((resolve) => { + res.write = vi.fn().mockImplementation(() => { + callOrder.push("write"); + return true; + }); + res.end = vi.fn().mockImplementation(() => { + callOrder.push("end"); + resolve(); + return res; + }); + }); + + const encoder = new TextEncoder(); + const chunks = ["chunk1", "chunk2", "chunk3"]; + let i = 0; + const stream = new ReadableStream({ + pull(controller) { + if (i < chunks.length) { + controller.enqueue(encoder.encode(chunks[i++])); + } else { + controller.close(); + } + }, + }); + + const webResponse = new Response(stream, { + status: 200, + headers: { "Content-Type": "text/plain" }, + }); + + await setResponse(res, webResponse); + await endPromise; + + // res.write must have been called for each chunk + expect(res.write).toHaveBeenCalledTimes(3); + // res.end() must be called exactly once + expect(res.end).toHaveBeenCalledTimes(1); + // res.end() must come after all writes + expect(callOrder).toEqual(["write", "write", "write", "end"]); + }); });