Skip to content

serve: JSON payload for the development error page, restoring stack traces - #37081

Merged
Jarred-Sumner merged 4 commits into
mainfrom
claude/dev-error-page-json
Aug 7, 2026
Merged

serve: JSON payload for the development error page, restoring stack traces#37081
Jarred-Sumner merged 4 commits into
mainfrom
claude/dev-error-page-json

Conversation

@dylan-conway

Copy link
Copy Markdown
Member

What does this PR do?

Bun.serve({ development: true }) renders an HTML error page (the packages/bun-error overlay) when a handler throws. Its payload was still the legacy peechy binary encoding — a base64 blob decoded in the browser by src/fallback.ts plus the checked-in generated src/api/schema.js — and the encoder had lost stack traces, source lines and build messages, so the page showed only an error title (and an empty box for a syntax error in an imported file).

  • New DevErrorPage (src/runtime/server/DevErrorPage.rs) embeds the payload as JSON in <script type="application/json"> (with < escaped) inside src/runtime/server/dev-error-page.html. Frames (1-based line/column, matching the terminal), source lines and build/resolve messages are back.
  • bun_jsc::schema_apibun_jsc::exception_list: owned snapshots of a ZigException that now keep source line text; SourceURLFormatter stops emitting a trailing : when line/column are excluded.
  • Removed: the binary Writer/fallback types in bun_options_types, Runtime::Fallback in bun_js_parser, bun_ast::api::Log, src/fallback.ts and its codegen step, src/api/schema.{js,d.ts} (~4.5k lines), the peechy npm dep, and bun-error's dead websocket-era renderBuildFailure. bun-error gets a local schema.ts for the JSON shape.
  • bun_core::fmt::substitute_named is now the one {[name]s} templater (shared with bun init); src/codegen/replacements.ts inlines the loader table it imported from schema.js (byte-identical defines).

Follow-ups, not in here: bun-error's unused client-side renderRuntimeError/sourcemap path, and collapsing the schema::api mirror enums (which also owns the loader-table json5/md gap noted in replacements.ts).

How did you verify your code works?

  • New serve.test.ts case spawns a dev server and asserts the JSON payload for a thrown TypeError (exact frame incl. file/line/column, source line text), a syntax error in an imported module (message, location, line_text) and a failed package resolution; it fails on current canary and passes on this build. test/bake/dev/react-response.test.ts (reads the same payload), test/js/bun/runtime-error.test.ts and test/cli/init/init.test.ts pass.
  • Loaded the live 500 pages in headless Chrome: the overlay now shows message, file:line:col, highlighted source lines and the stack (before: title only / empty for build errors). Also drove HEAD (no body), a thrown string, and a message containing </script><script> (stays inside the JSON).
  • cargo clippy --workspace, Windows cross-check of bun_jsc/bun_js_parser.

…tack traces

`Bun.serve({ development: true })` answers a throwing request with an HTML
error page rendered by `packages/bun-error`. The page's payload was still the
old peechy binary encoding (a base64 blob decoded in the browser by
`src/fallback.ts` + the checked-in `src/api/schema.js`), and the encoder had
lost the exception stack traces, source lines and build/resolve messages
along the way, so the page showed an error title with nothing under it, and
a syntax error in an imported file showed an empty box.

- New `DevErrorPage` (src/runtime/server/DevErrorPage.rs) writes the payload
  as JSON into `<script type="application/json">` in the (moved) template
  `src/runtime/server/dev-error-page.html`, including frames with 1-based
  positions, trimmed source lines, and the request's build/resolve messages.
- `bun_jsc::schema_api` becomes `bun_jsc::exception_list`: plain owned
  snapshots of a `ZigException` (`ZigStackTrace::snapshot`), now carrying the
  source line text. `SourceURLFormatter` no longer prints a trailing `:` when
  line/column are excluded, and origin remapping follows the same rule as
  the terminal printer.
- Deleted: the binary `Writer` and fallback types in `bun_options_types`,
  `bun_js_parser::parser::Runtime::Fallback`, `bun_ast::api::Log`,
  `src/fallback.ts` + its codegen step, `src/api/schema.{js,d.ts}`, the
  `peechy` npm dependency, and bun-error's websocket-era `renderBuildFailure`.
  bun-error gets a small local `schema.ts` describing the JSON.
- `bun_core::fmt::substitute_named` is the `{[name]s}` templater shared by
  `bun init` and the error page; `src/codegen/replacements.ts` inlines the
  loader table it used to import from schema.js.
- Test: dev error page payload for a thrown error, a syntax error and a
  failed resolution (test/js/bun/http/serve.test.ts).
@dylan-conway
dylan-conway requested a review from alii as a code owner August 7, 2026 01:23
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f20403fa-0a07-4ca1-a24c-6c5e1e2c854a

📥 Commits

Reviewing files that changed from the base of the PR and between dd940c6 and b06b256.

📒 Files selected for processing (2)
  • packages/bun-error/schema.ts
  • src/runtime/server/DevErrorPage.rs

Walkthrough

Changes

The PR replaces the binary fallback error-page protocol with native exception snapshots and server-rendered development error pages. It removes fallback decoding, obsolete schemas, related dependencies, and legacy rendering paths. Tests validate embedded JSON error responses.

Development error page migration

Layer / File(s) Summary
Exception data contracts
packages/bun-error/schema.ts, src/jsc/lib.rs, src/options_types/schema.rs
Added TypeScript and Rust exception structures. Removed the legacy fallback wire structures.
Native exception snapshots
src/jsc/VirtualMachine.rs, src/jsc/ZigException.rs, src/jsc/ZigStackFrame.rs, src/jsc/ZigStackTrace.rs
JSC now creates owned exception snapshots with typed codes, stack frames, source lines, trimmed text, and conditional development-server origins.
Server-rendered error page
src/runtime/server/DevErrorPage.rs, src/runtime/server/dev-error-page.html, src/runtime/server/RequestContext.rs, packages/bun-error/*
RequestContext renders DevErrorPage with native exceptions, logs, and message bytes. The page serializes escaped JSON and embeds renderer assets.
Fallback and build-path removal
scripts/build/codegen.ts, src/runtime/cli/init_command.rs, src/bun_core/fmt.rs, src/codegen/replacements.ts, src/runtime/error.rs, scripts/glob-sources.ts, package.json, test/package.json, .gitignore, .gitattributes
Removed fallback decoder generation, obsolete error handling, and the peechy dependency. Added shared named-template substitution and updated loader generation and source globs.
Development error page validation
test/js/bun/http/serve.test.ts, test/bake/dev/react-response.test.ts, test/internal/source-lints/dead-symbols-pub-exports-sweep.test.ts
Tests parse JSON payloads and cover runtime errors, syntax errors, unresolved imports, source locations, escaping, and renderer output.

Possibly related PRs

Suggested reviewers: alii

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: JSON payloads for development error pages and restored stack traces.
Description check ✅ Passed The description includes both required sections and provides clear implementation details and verification results.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

@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: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/bun_core/fmt.rs`:
- Around line 433-455: Add regression tests for substitute_named covering exact
named replacements, adjacent placeholders, unmatched placeholders remaining
unchanged, and malformed markers being copied through without corruption.
Exercise both empty and non-empty surrounding text as needed, preserving the
helper’s documented behavior used by DevErrorPage HTML and generated project
files.

In `@src/codegen/replacements.ts`:
- Around line 91-117: Add “json5” and “md” to the Loader array in enums, in that
exact order after “yaml” so they occupy schema IDs 20 and 21. Keep the generated
LoaderIdToLabel and LoaderLabelToId mappings aligned with these entries.

In `@src/runtime/server/dev-error-page.html`:
- Around line 2-6: Update the root html element in the error page to include
lang="en", and add a stable title element within head alongside the existing
metadata so assistive technology can identify the page.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 94a7f353-5940-497d-98c1-ad9eb7d0eccc

📥 Commits

Reviewing files that changed from the base of the PR and between 898b169 and 3f8949e.

⛔ Files ignored due to path filters (3)
  • Cargo.lock is excluded by !**/*.lock
  • bun.lock is excluded by !**/*.lock
  • test/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (37)
  • .gitattributes
  • .gitignore
  • package.json
  • packages/bun-error/index.tsx
  • packages/bun-error/markdown.ts
  • packages/bun-error/runtime-error.ts
  • packages/bun-error/schema.ts
  • packages/bun-error/stack-trace-parser.ts
  • scripts/build/codegen.ts
  • scripts/glob-sources.ts
  • src/api/schema.d.ts
  • src/api/schema.js
  • src/ast/lib.rs
  • src/bun_core/fmt.rs
  • src/codegen/replacements.ts
  • src/fallback-backend.html
  • src/fallback.ts
  • src/js_parser/Cargo.toml
  • src/js_parser/parser.rs
  • src/jsc/VirtualMachine.rs
  • src/jsc/ZigException.rs
  • src/jsc/ZigStackFrame.rs
  • src/jsc/ZigStackTrace.rs
  • src/jsc/lib.rs
  • src/options_types/schema.rs
  • src/runtime/bake/bake_body.rs
  • src/runtime/cli/init_command.rs
  • src/runtime/error.rs
  • src/runtime/server/DevErrorPage.rs
  • src/runtime/server/RequestContext.rs
  • src/runtime/server/dev-error-page.html
  • src/runtime/server/mod.rs
  • src/runtime/server/server_body.rs
  • test/bake/dev/react-response.test.ts
  • test/internal/source-lints/dead-symbols-pub-exports-sweep.test.ts
  • test/js/bun/http/serve.test.ts
  • test/package.json
💤 Files with no reviewable changes (12)
  • .gitattributes
  • src/fallback-backend.html
  • src/js_parser/Cargo.toml
  • src/runtime/error.rs
  • test/package.json
  • .gitignore
  • scripts/build/codegen.ts
  • package.json
  • src/fallback.ts
  • src/js_parser/parser.rs
  • src/ast/lib.rs
  • src/api/schema.d.ts

Comment thread src/bun_core/fmt.rs
Comment thread src/codegen/replacements.ts
Comment thread src/runtime/server/dev-error-page.html Outdated
main updated src/api/schema.{js,d.ts} (json5/md/xml loader ids) and made
schema.js an explicit input of the JS-modules codegen edge; this branch
deletes those files. Keep the deletion, add the three loader names to the
inlined table in src/codegen/replacements.ts (generated defines are
identical to main's), and drop the schema.js input from
scripts/build/codegen.ts.

@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 (2)
src/js_parser/parser.rs (1)

760-761: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add a regression test for hyphenated JSX tags.

Existing tests cover namespaced tags and member expressions. Add a case such as <Hello-Button />.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/js_parser/parser.rs` around lines 760 - 761, Add a regression test
covering a hyphenated JSX tag such as <Hello-Button /> alongside the existing
namespaced-tag and member-expression tests, verifying it parses successfully and
preserves the expected tag name.

Source: Coding guidelines

src/runtime/cli/init_command.rs (1)

1920-1920: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add bun init entry-point regression tests.

Cover ../entry, ..\\entry, /absolute/entry, C:\\absolute\\entry, and a safe nested path. Assert that unsafe paths do not create files and the nested path does.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/cli/init_command.rs` at line 1920, Add regression tests for the
bun init entry-point validation around is_absolute_loose and split_any, covering
../entry, ..\entry, /absolute/entry, C:\absolute\entry, and a safe nested path.
Assert that each unsafe path creates no files, while the safe nested path
creates the expected files.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/js_parser/parser.rs`:
- Around line 760-761: Add a regression test covering a hyphenated JSX tag such
as <Hello-Button /> alongside the existing namespaced-tag and member-expression
tests, verifying it parses successfully and preserves the expected tag name.

In `@src/runtime/cli/init_command.rs`:
- Line 1920: Add regression tests for the bun init entry-point validation around
is_absolute_loose and split_any, covering ../entry, ..\entry, /absolute/entry,
C:\absolute\entry, and a safe nested path. Assert that each unsafe path creates
no files, while the safe nested path creates the expected files.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c9728aa7-4372-4728-9d57-7e4dd5c66c11

📥 Commits

Reviewing files that changed from the base of the PR and between 3f8949e and 82c08cb.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (7)
  • .gitattributes
  • src/bun_core/fmt.rs
  • src/codegen/replacements.ts
  • src/js_parser/parser.rs
  • src/options_types/schema.rs
  • src/runtime/cli/init_command.rs
  • src/runtime/server/RequestContext.rs

Comment thread src/runtime/server/DevErrorPage.rs Outdated
bun-error hides a frame's line/column when they are -1; an invalid Ordinal
was being serialized as one_based() == 0 and rendered as ':0:0'.

@claude claude 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.

Thanks for the fix in b06b256one_based_or_missing now emits -1 for frames without a source position, matching bun-error's sentinel. No further issues found, but given the scope (new hand-written JSON serializer embedded in HTML, changes across RequestContext/ZigException/VirtualMachine error paths, ~4.5k lines of legacy encoding removed) this should get a human look.

What was reviewed:

  • escape_for_script_element<\u003c prevents </script>/<!-- breakout; only occurs inside JSON strings so the escape is valid.
  • substitute_named move to bun_core::fmt — same semantics as the removed init_command::substitute; covered by init tests and the new unfilledPlaceholder assertion.
  • Inlined Loader table in replacements.ts — ids 1–22 match api::Loader discriminants; generated $LoaderIdToLabel/$LoaderLabelToId are byte-identical to the deleted schema.js output.
  • origin now gated on is_from_devserver in add_to_error_list callers — only affects the URL prefix in dev-error-page frame paths.
Extended reasoning...

Overview

This PR replaces the legacy peechy binary encoding for Bun.serve({ development: true })'s error page with a JSON payload embedded in <script type="application/json">, restoring stack traces, source lines, and build/resolve messages that the old encoder had lost. It adds src/runtime/server/DevErrorPage.rs (hand-written JSON serializer + HTML template substitution), renames bun_jsc::schema_apiexception_list with owned snapshots that now carry source-line text, consolidates the {[name]s} templater into bun_core::fmt::substitute_named, and deletes ~4.5k lines of dead peechy scaffolding (src/api/schema.{js,d.ts}, src/fallback.ts, the Writer/fallback types in bun_options_types, Runtime::Fallback, the peechy npm dep). packages/bun-error gets a local schema.ts and drops the websocket-era renderBuildFailure path.

Security risks

The main surface is user-controlled error content (message, stack, file paths, source lines) serialized as JSON and embedded in a <script> element. escape_for_script_element replaces every < with \u003c, which defeats both </script> and <!-- parser-state changes; since < can only appear inside JSON string literals (all other output is structural or numeric), the escape is always inside a quoted string where \u003c is equivalent. format_json_string_utf8 handles quote/backslash/control escaping. This is dev-mode-only (development: true), which bounds the blast radius. I did not find an injection path.

Level of scrutiny

High. 38 files across Rust core (jsc bindings, VirtualMachine, RequestContext), the bun-error preact frontend, codegen (replacements.ts loader table), and build scripts. The JSON serializer is hand-rolled rather than serde-derived, so field ordering and escaping are manual. Two behavioral tweaks beyond the headline change: SourceURLFormatter now suppresses the trailing : when exclude_line_column is set (fixes a latent formatting bug), and origin is now only passed to add_to_error_list when is_from_devserver is true (previously unconditional). Neither looks wrong, but both are the kind of thing a maintainer should confirm was intentional.

Other factors

My one prior finding (invalid Ordinal positions serializing as 0 instead of -1) was addressed in b06b256 with one_based_or_missing. All CodeRabbit threads are resolved. The new serve.test.ts case is thorough — it asserts the exact JSON shape for a thrown TypeError (frame file/line/column, source lines), a syntax error (build message with line_text), and a resolve error (specifier in on.resolve), plus that no template placeholder survives and the renderer bundle is present. react-response.test.ts was updated to read the new format. Given the breadth and the number of subsystems touched, a maintainer sign-off is appropriate even with clean automated review.

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.

2 participants