Skip to content

http: escape control characters in the verbose request/response trace - #38673

Open
robobun wants to merge 1 commit into
mainfrom
farm/60387fcd/escape-verbose-http-trace
Open

http: escape control characters in the verbose request/response trace#38673
robobun wants to merge 1 commit into
mainfrom
farm/60387fcd/escape-verbose-http-trace

Conversation

@robobun

@robobun robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Problem

Fix

  • Adds bun_core::fmt::escape_control_chars / EscapeControlChars (the same hunk as install: escape control characters in resolutions, specifiers, bin names and registry error text #38631, install: escape control characters in the bun pm untrusted/trust script listing #38525 and install: reject dependency names containing control characters #38615, so whichever lands first the others merge cleanly) and prints every peer- or dependency-supplied field of the trace through it: the request URL, header names and values in both the >/< lines and the curl line, the redacted Authorization scheme, and the response status text. Headers (used to build the WebSocket upgrade request) and the method (a Method enum name) are untouched.
  • Escaping happens inside the Display impls rather than around them, because those impls also emit bun's own ANSI colour codes when stderr is a TTY; only the data is escaped (checked by hand with FORCE_COLOR=1).
  • The request line keeps its secret redaction and wraps it: EscapeControlChars(redacted_npm_url(url)), so redaction runs on the real bytes and escaping on the result. For that to be sound RedactedNpmUrlFormatter now writes its runs through BStr instead of write_bytes (from_utf8_unchecked): bun install hands the trace whatever bytes the package.json contained, and a Latin-1 file gets bytes that are not UTF-8 (verified: a dependency URL containing 82 41 9B 41 reaches print_request as-is). Runs only ever split at ASCII bytes, so valid UTF-8 renders exactly as before; invalid sequences now render as U+FFFD, which is how the package manager's own error lines already print them.
  • Why this is the right output: the trace exists to show what went over the wire, so the bytes are spelled out (\u009b, \x1b, \r) rather than dropped, matching the convention the sibling PRs establish for the rest of the install output. The request body in the curl line is already a JSON string literal (C0 controls escaped) and is the caller's own data, so it is left alone.
  • Verified with:
    • test/js/web/fetch/fetch.test.ts "verbose fetch logging escapes control characters coming from the peer": raw TCP server sends a reason phrase and a header value containing U+009B, the client sends x-req and Authorization values containing it, verbose: "curl". Covers the > header lines, the redacted Authorization branch, the curl line and the < status and header lines. Fails on the released binary (the raw U+009B is printed in all five places), passes with this branch.
    • test/cli/install/bun-install.test.ts "--verbose escapes control characters in the tarball URL and response headers it traces": package.json with one tarball URL containing U+009B and one containing raw non-UTF-8 bytes, against a local 404 server. Covers the request-line URL path (which fetch() cannot reach, since it percent-encodes the path) and the non-UTF-8 rendering. Fails on the released binary, passes with this branch.
    • bun bd test test/regression/issue/12042.test.ts (curl trace body) and test/cli/install/redacted-config-logs.test.ts (URL redaction) still pass; the remaining failures when running the two edited files in full here are localhost/internet-dependent tests that fail identically with the released binary.

Background

  • The verbose trace is printed from the HTTP client thread by print_request (HTTP/1.1, h2 and h3 all call it) and print_response in src/http/lib.rs; the line formatting for headers and the response lives in the Display impls of the bun_picohttp crate. HTTPVerboseLevel::Headers prints the >/< lines, HTTPVerboseLevel::Curl additionally prints a copy-pasteable curl command first; bun install --verbose uses Headers.
  • C1 controls are U+0080..U+009F. Terminals treat several of them as one-byte forms of two-byte ESC sequences (U+009B = ESC [, CSI; U+009D = ESC ], OSC), and in UTF-8 mode xterm and others recognise them when they arrive UTF-8 encoded (C2 9B), which is the form that passes through an HTTP/1.1 parser.
  • bstr::BStr's Display writes bytes as UTF-8 and substitutes U+FFFD for invalid sequences, which is what the rest of the trace already used; escape_control_chars(bytes) builds on it, and EscapeControlChars<T: Display> escapes whatever the inner Display writes, so it must only ever be fed valid &str chunks (the reason for the RedactedNpmUrlFormatter change).
  • redacted_npm_url replaces URL passwords, UUIDs and npm_ tokens with ***; it is the existing formatter for printing registry/tarball URLs.

The trace printed for fetch(..., { verbose }) and bun install --verbose wrote
the request URL, header names and values, and the response status text to
stderr byte for byte. HTTP/1.1 lets a peer put any byte >= 0x80 (including
UTF-8 encoded C1 controls such as U+009B, the one-byte CSI) in a field value
or reason phrase, HPACK/QPACK only reject NUL, CR and LF, and the tarball URL
bun install prints comes straight out of a dependency's metadata, so a peer or
a dependency could emit terminal control sequences through the trace.

Print those fields through bun_core::fmt::escape_control_chars, which spells
out C0 controls, DEL and C1 controls, and have redacted_npm_url render its
runs via BStr so a URL that is not valid UTF-8 shows up as U+FFFD instead of
being reinterpreted as a str inside the escaping wrapper.
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 5 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 64f15d42-83b0-477b-9d56-f6474e98bb2c

📥 Commits

Reviewing files that changed from the base of the PR and between 9cff2a1 and 8f47acb.

📒 Files selected for processing (5)
  • src/bun_core/fmt.rs
  • src/http/lib.rs
  • src/picohttp/lib.rs
  • test/cli/install/bun-install.test.ts
  • test/js/web/fetch/fetch.test.ts

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

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Status

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.

1 participant