Skip to content

fix: move res.end() outside stream reading loop in setResponse#129

Closed
roor0 wants to merge 1 commit intobetter-auth:mainfrom
roor0:fix/response-truncation
Closed

fix: move res.end() outside stream reading loop in setResponse#129
roor0 wants to merge 1 commit intobetter-auth:mainfrom
roor0:fix/response-truncation

Conversation

@roor0
Copy link
Copy Markdown

@roor0 roor0 commented Apr 13, 2026

Problem

setResponse() in the Node.js adapter calls res.end() inside the stream reading loop (after a successful res.write()), causing the HTTP response to be terminated after the first chunk (~16KB). Any response body larger than one chunk is silently truncated, returning incomplete/malformed JSON to the client.

This affects applications using better-auth's customSession plugin where the session payload exceeds 16KB (common with production user data containing assignments, projects, divisions, etc.). The client receives truncated JSON, parses it as null, and the user appears unauthenticated.

Root Cause

// Before (broken):
async function next() {
    try {
        for (;;) {
            const { done, value } = await reader.read();
            if (done) break;
            if (!res.write(value)) {
                // backpressure handling...
            }
            res.end(); // ← called inside loop, ends response after first chunk
        }
    } catch (error) { ... }
}

Fix

Move res.end() after the loop:

// After (fixed):
async function next() {
    try {
        for (;;) {
            const { done, value } = await reader.read();
            if (done) break;
            if (!res.write(value)) {
                // backpressure handling...
            }
        }
        res.end(); // ← called after loop, full response is written
    } catch (error) { ... }
}

Test

Added a test that creates a response body larger than 16KB and verifies the full body is written to the Node.js response without truncation.

res.end() was called inside the for loop after a successful res.write(),
causing the response to be terminated after the first chunk. This
truncates any response body larger than one chunk (~16KB), returning
incomplete JSON to the client.

This breaks applications with large response payloads (e.g. better-auth
customSession with production user data exceeding 16KB).

The fix moves res.end() after the loop so the full response body is
streamed before ending.
roor0 added a commit to roor0/better-auth that referenced this pull request Apr 13, 2026
The client's JSON parser was configured with `strict: false`, which
causes truncated JSON responses to be silently returned as raw strings
instead of throwing an error. This means a response body truncated at
a chunk boundary (e.g. due to the res.end() bug in better-call#129)
produces a string value that the client treats as valid data, making
the failure completely invisible — no error, no warning.

With `strict: true`, the JSON parse error surfaces as an actual error,
which the client query layer handles properly via `onError`.

See: better-auth/better-call#129
roor0 added a commit to roor0/better-auth that referenced this pull request Apr 13, 2026
The client's JSON parser was configured with `strict: false`, which
causes truncated JSON responses to be silently returned as raw strings
instead of throwing an error. This means a response body truncated at
a chunk boundary (e.g. due to the res.end() bug in better-call#129)
produces a string value that the client treats as valid data, making
the failure completely invisible — no error, no warning.

With `strict: true`, the JSON parse error surfaces as an actual error,
which the client query layer handles properly via `onError`.

See: better-auth/better-call#129
roor0 added a commit to roor0/better-auth that referenced this pull request Apr 13, 2026
The client's JSON parser was configured with `strict: false`, which
causes truncated JSON responses to be silently returned as raw strings
instead of throwing an error. This means a response body truncated at
a chunk boundary (e.g. due to the res.end() bug in better-call#129)
produces a string value that the client treats as valid data, making
the failure completely invisible — no error, no warning.

With `strict: true`, the JSON parse error surfaces as an actual error,
which the client query layer handles properly via `onError`.

See: better-auth/better-call#129
@roor0
Copy link
Copy Markdown
Author

roor0 commented Apr 13, 2026

Closing — this is a duplicate of #124 which already addresses the same res.end() placement bug. Thanks!

@roor0 roor0 closed this Apr 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant