Skip to content

fix: accept \r as line ending in alert/confirm/prompt for REPL compatibility - #27544

Closed
robobun wants to merge 2 commits into
mainfrom
claude/fix-repl-alert-hang-27542
Closed

robobun wants to merge 2 commits into
mainfrom
claude/fix-repl-alert-hang-27542

Conversation

@robobun

@robobun robobun commented Feb 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Fix alert(), confirm(), and prompt() hanging indefinitely when called from bun repl
  • The REPL puts the terminal in raw mode where Enter sends \r instead of \n, but these functions only checked for \n
  • Now all three functions accept both \r and \n as line endings

Closes #27542

Test plan

  • New regression tests in test/regression/issue/27542.test.ts verify all three functions work with \r:
    • alert() accepts \r to dismiss
    • confirm() accepts bare \r as default (no)
    • confirm() accepts y + \r as yes
    • prompt() accepts \r to return default value
    • prompt() accepts text + \r to return user input
  • Tests fail with system bun (USE_SYSTEM_BUN=1), pass with debug build
  • Existing web-globals.test.js confirm/prompt tests still pass (18/18)

🤖 Generated with Claude Code

…ibility

When the REPL puts the terminal in raw mode, pressing Enter sends \r
(carriage return) instead of \n (newline). The alert(), confirm(), and
prompt() functions only checked for \n, causing them to hang
indefinitely when called from the REPL.

Closes #27542

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@robobun

robobun commented Feb 28, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 1:29 AM PT - Mar 1st, 2026

❌ Your commit 9e18af97 has 2 failures in Build #38408 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 27544

That installs a local version of the PR into your bun-27544 executable, so you can run:

bun-27544 --bun

@coderabbitai

coderabbitai Bot commented Feb 28, 2026 •

Copy link
Copy Markdown
Contributor

Walkthrough

Adds CR (\r) and CRLF (\r\n) handling to prompt APIs (alert/confirm/prompt) for raw-terminal input; introduces streamed body byte accounting and a 413 rejection helper to enforce max request body size; and adds tests for prompt CR handling and chunked-request 413 behavior.

Changes

Cohort / File(s) Summary
Prompt Functionality
src/bun.js/webcore/prompt.zig
Treat \r and \r\n as valid line terminators across alert, confirm, prompt and associated read utilities; remove second-byte tracking and consistently consume optional trailing byte when CR encountered.
Prompt Regression Tests
test/regression/issue/27542.test.ts
New tests verifying alert(), confirm(), and prompt() accept \r and \r\n as Enter/default inputs and that sequential prompts don't leak input.
Request Body Size Enforcement
src/bun.js/api/server/RequestContext.zig
Add streamed_body_bytes_received public field and rejectRequestBodyAsEntityTooLarge method; increment and initialize streamed-byte counter during buffered/streaming body processing and reject with 413 when limit exceeded.
Chunked Request 413 Tests
test/js/bun/http/serve-chunked-413.test.ts
Adds three tests exercising chunked POST handling against maxRequestBodySize: exceeding limit yields 413 (raw socket and ReadableStream), and within limit succeeds with 200.
🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The changes include out-of-scope modifications: RequestContext.zig adds maxRequestBodySize enforcement for chunked requests (413 error handling) unrelated to the REPL line-ending issue, and corresponding test file added. Remove or separate the maxRequestBodySize enforcement changes (RequestContext.zig and serve-chunked-413.test.ts) into a dedicated PR, keeping only the REPL-related alert/confirm/prompt fixes and their regression tests.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately and concisely describes the main change: accepting '\r' as a line ending in alert/confirm/prompt for REPL compatibility, addressing issue #27542.
Description check ✅ Passed The PR description follows the template with both required sections (What does this PR do and How did you verify) completed, providing clear context about the fix, root cause, and comprehensive test coverage.
Linked Issues check ✅ Passed The code changes successfully address issue #27542 by implementing support for '\r' and '\r\n' line endings in alert(), confirm(), and prompt() functions, with regression tests validating the fix.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/bun.js/webcore/prompt.zig (1)

155-162: 🧹 Nitpick | 🔵 Trivial

Consider the semantic coupling of hardcoded \r.

The function now always terminates on \r regardless of the delimiter parameter. This works correctly for the current usage (only called with delimiter='\n'), but could cause unexpected behavior if the function is ever reused with a different delimiter.

The comment at lines 155-156 documents this, which is good. Just be aware this is now effectively a "line reader" rather than a generic delimiter reader.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/bun.js/webcore/prompt.zig` around lines 155 - 162, The reader currently
treats '\r' as an unconditional terminator which couples behaviour to newline
semantics; update the condition that checks byte vs delimiter so that '\r' is
treated as an alternative terminator only when delimiter == '\n' (i.e., accept
'\r' as equivalent to '\n' but do not unconditionally return on '\r'); locate
the comparison using the variables delimiter and byte (near
array_list.appendAssumeCapacity) and change it to only accept '\r' when the
requested delimiter is '\n'.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/bun.js/webcore/prompt.zig`:
- Around line 155-162: The reader currently treats '\r' as an unconditional
terminator which couples behaviour to newline semantics; update the condition
that checks byte vs delimiter so that '\r' is treated as an alternative
terminator only when delimiter == '\n' (i.e., accept '\r' as equivalent to '\n'
but do not unconditionally return on '\r'); locate the comparison using the
variables delimiter and byte (near array_list.appendAssumeCapacity) and change
it to only accept '\r' when the requested delimiter is '\n'.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between a7788dc and bf76a8e.

📒 Files selected for processing (2)
  • src/bun.js/webcore/prompt.zig
  • test/regression/issue/27542.test.ts

Comment thread src/bun.js/webcore/prompt.zig Outdated
Comment on lines +112 to +113
switch (first_byte) {
'\n' => return .false,
'\r' => {
const next_byte = reader.takeByte() catch {
// They may have said yes, but the stdin is invalid.
return .false;
};
if (next_byte == '\n') {
return .false;
}
},
'\n', '\r' => return .false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 On Windows in cooked console mode (non-REPL), pressing Enter sends \r\n. The new code returns immediately on \r, leaving \n in the stdin buffer. This means sequential calls like confirm("first?"); confirm("second?") will have the second call immediately consume the leftover \n and return without waiting for user input. The fix should consume an optional trailing \n after \r (as the old code did), rather than treating \r as a standalone terminator.

Extended reasoning...

What the bug is

The PR replaces explicit \r\n (Windows CRLF) pair handling with simply treating \r as a standalone line terminator, equivalent to \n. While this correctly fixes the REPL case (where raw terminal mode sends bare \r), it regresses the normal Windows console case where Enter sends \r\n as a two-byte sequence.

How it manifests across all three functions

alert(): The old loop if (byte == '\n') break would read \r (no match, continue), then read \n (match, break) - correctly consuming both bytes. The new loop if (byte == '\n' or byte == '\r') break reads \r and immediately breaks, leaving \n in the stdin buffer.

confirm(): The old code had an explicit '\r' switch case that read the next byte and checked for \n before returning. The new code '\n', '\r' => return .false returns immediately on \r. Similarly, after y/Y, the old code checked for \r followed by \n; the new code returns .true on bare \r.

prompt(): The old code had an explicit else if (first_byte == '\r') branch that read a second byte and checked for \n. The new code returns default immediately on \r. The readUntilDelimiter helpers also now break on \r, leaving \n in the buffer.

Step-by-step proof with a concrete example

Consider this script running on Windows in a normal console (cooked mode):

const a = confirm("first?");  // User presses Enter
const b = confirm("second?"); // Should wait for user input
console.log(a, b);
  1. User sees first? [y/N] and presses Enter, which sends \r\n to stdin.
  2. confirm() reads first_byte = '\r', matches the '\n', '\r' case, and returns immediately. The \n byte remains in the stdin buffer.
  3. User sees second? [y/N] but confirm() immediately reads first_byte = '\n' (the leftover byte), matches the same case, and returns .false without waiting.
  4. Output: false false but the user only pressed Enter once.

The same issue affects prompt() via buffered_stdin (which persists across calls) and alert() followed by any other stdin-reading function.

Why existing tests don't catch this

The new tests only send bare \r (simulating raw REPL mode) and only test single calls. There are no tests for sequential calls, and no tests sending \r\n (simulating Windows cooked mode).

Impact

This is a regression for all Windows users running scripts that use multiple sequential alert(), confirm(), or prompt() calls in normal (non-REPL) console mode. The second call in any sequence will silently consume the leftover \n and return immediately with a default value.

How to fix

After breaking on \r, attempt to read the next byte and consume it if it is \n (restoring the old behavior). This handles both cases: bare \r in raw REPL mode and \r\n in Windows cooked mode. For example, in the alert loop, instead of if (byte == '\n' or byte == '\r') break, use logic that on seeing \r peeks at the next byte and consumes it if it is \n. Similar patterns should be applied in confirm(), prompt(), and both readUntilDelimiter helpers.

…or chunked requests

1. Fix Windows \r\n regression in alert/confirm/prompt: when \r is encountered,
   consume the optional trailing \n to prevent it leaking into subsequent calls.
   This handles both bare \r (REPL raw mode) and \r\n (Windows cooked mode).

2. Enforce maxRequestBodySize for chunked/streaming request bodies: track total
   bytes received via streamed_body_bytes_received and reject with 413 when the
   limit is exceeded. Previously only Content-Length was checked upfront, allowing
   chunked requests to bypass the limit entirely.

3. Initialize streamed_body_bytes_received with pre-buffered byte count when
   transitioning from buffered to streaming mode in onStartStreamingRequestBody.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/js/bun/http/serve-chunked-413.test.ts`:
- Around line 61-73: Replace uses of string.repeat for large repetitive payloads
with Buffer.alloc(...).toString() to avoid relying on String.repeat: update the
construction of the request variable that currently uses "A".repeat(40) and
"B".repeat(40) to use Buffer.alloc(40, "A").toString() and Buffer.alloc(40,
"B").toString() respectively, and apply the same replacement for the identical
repeat patterns referenced later in the file (the occurrences at the same shapes
around the blocks noted at the other two locations).

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between bf76a8e and 9e18af9.

📒 Files selected for processing (4)
  • src/bun.js/api/server/RequestContext.zig
  • src/bun.js/webcore/prompt.zig
  • test/js/bun/http/serve-chunked-413.test.ts
  • test/regression/issue/27542.test.ts

Comment on lines +61 to +73
const request =
"POST / HTTP/1.1\r\n" +
`Host: ${server.hostname}:${server.port}\r\n` +
"Transfer-Encoding: chunked\r\n" +
"\r\n" +
"28\r\n" + // 0x28 = 40 in hex
"A".repeat(40) +
"\r\n" +
"28\r\n" + // 0x28 = 40 in hex
"B".repeat(40) +
"\r\n" +
"0\r\n" +
"\r\n";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Use Buffer.alloc instead of String.repeat for repetitive strings.

As per coding guidelines, use Buffer.alloc(count, fill).toString() instead of 'A'.repeat(count) to create repetitive strings in tests.

♻️ Proposed fix
   const request =
     "POST / HTTP/1.1\r\n" +
     `Host: ${server.hostname}:${server.port}\r\n` +
     "Transfer-Encoding: chunked\r\n" +
     "\r\n" +
     "28\r\n" + // 0x28 = 40 in hex
-    "A".repeat(40) +
+    Buffer.alloc(40, "A").toString() +
     "\r\n" +
     "28\r\n" + // 0x28 = 40 in hex
-    "B".repeat(40) +
+    Buffer.alloc(40, "B").toString() +
     "\r\n" +
     "0\r\n" +
     "\r\n";

The same change should be applied to the identical patterns at lines 147-150 and 215-218.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const request =
"POST / HTTP/1.1\r\n" +
`Host: ${server.hostname}:${server.port}\r\n` +
"Transfer-Encoding: chunked\r\n" +
"\r\n" +
"28\r\n" + // 0x28 = 40 in hex
"A".repeat(40) +
"\r\n" +
"28\r\n" + // 0x28 = 40 in hex
"B".repeat(40) +
"\r\n" +
"0\r\n" +
"\r\n";
const request =
"POST / HTTP/1.1\r\n" +
`Host: ${server.hostname}:${server.port}\r\n` +
"Transfer-Encoding: chunked\r\n" +
"\r\n" +
"28\r\n" + // 0x28 = 40 in hex
Buffer.alloc(40, "A").toString() +
"\r\n" +
"28\r\n" + // 0x28 = 40 in hex
Buffer.alloc(40, "B").toString() +
"\r\n" +
"0\r\n" +
"\r\n";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/js/bun/http/serve-chunked-413.test.ts` around lines 61 - 73, Replace
uses of string.repeat for large repetitive payloads with
Buffer.alloc(...).toString() to avoid relying on String.repeat: update the
construction of the request variable that currently uses "A".repeat(40) and
"B".repeat(40) to use Buffer.alloc(40, "A").toString() and Buffer.alloc(40,
"B").toString() respectively, and apply the same replacement for the identical
repeat patterns referenced later in the file (the occurrences at the same shapes
around the blocks noted at the other two locations).

Comment on lines +56 to +60
if (byte == '\r') {
// Consume optional trailing \n for Windows \r\n line endings
_ = reader.takeByte() catch {};
break;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 The _ = reader.takeByte() catch {} after reading \r (line 58, and 5 other locations) performs a blocking read. In raw terminal mode (the REPL scenario this PR fixes), Enter sends only \r with no trailing \n, so this read blocks until the user presses a second key — partially defeating the fix. The follow-up read should only consume the next byte if data is already buffered (non-blocking peek), not issue an unconditional blocking read.

Extended reasoning...

What the bug is

The PR adds _ = reader.takeByte() catch {} (or readByte) after every \r detection to consume an optional trailing \n for Windows CRLF line endings. However, takeByte() and readByte() are blocking calls — they issue a read() syscall that blocks until at least one byte is available. In raw terminal mode (the exact REPL scenario this PR is designed to fix), pressing Enter sends only \r with no trailing \n, so the follow-up read blocks waiting for the next byte from stdin.

The specific code path

In alert() at lines 56-60:

if (byte == 
) {
    // Consume optional trailing \n for Windows \r\n line endings
    _ = reader.takeByte() catch {};
    break;
}

The reader wraps a std.fs.File (stdin) via readerStreaming with a 1-byte buffer. When all buffered data has been consumed (only \r was available), takeByte() calls the underlying read() syscall. The catch {} only handles read errors (e.g., EOF on a pipe), not blocking — a blocking read simply never returns.

Why the REPL triggers this

The REPL puts the terminal in raw mode at startup (bun.tty.setMode(0, .raw) in repl.zig:757). The raw mode configuration sets VMIN=1, VTIME=0 (confirmed in wtf-bindings.cpp:90), meaning read() blocks until at least 1 byte is available. When the user presses Enter in raw mode, only \r is sent — there is no trailing \n. So after consuming \r, the takeByte() call blocks indefinitely waiting for the next keystroke.

Step-by-step proof

  1. User starts bun repl (terminal enters raw mode)
  2. User types alert("hello") and submits it
  3. Bun prints hello [Enter] and waits for input
  4. User presses Enter, which sends \r (just one byte in raw mode)
  5. The while loop reads byte = \r at line 54
  6. Enters the if (byte == ) branch at line 56
  7. Line 58: _ = reader.takeByte() catch {} issues a blocking read() syscall
  8. No data is available (raw mode only sent \r), so read() blocks
  9. The user must press a second key to unblock the read (that key is silently discarded)

Before this PR, alert() hung indefinitely (infinite loop waiting for \n). After this PR, it hangs until a second keypress — an improvement but still broken.

Why the tests pass

All tests use piped stdin with proc.stdin.end() after writing \r. When the pipe is closed, the follow-up takeByte() gets EOF, which is caught by catch {}. But in a real REPL session, stdin is an open terminal file descriptor — it never returns EOF, so the read blocks.

How to fix

The fix should only consume the next byte if it is already available in the reader's internal buffer (a non-blocking peek). For example, after reading \r, check if the streaming reader has any buffered data before attempting to read the trailing byte. If no data is buffered, skip the read. This handles both cases correctly: bare \r in raw REPL mode (no blocking read) and \r\n in Windows cooked mode (consumes the \n from the buffer since both bytes arrive together). The same fix is needed at all six locations: alert() line 58, confirm() lines 121/137/147, both readUntilDelimiter helpers at lines 176/199, and prompt() at line 289.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bun repl hanged by alert

1 participant