Skip to content

bake: return undefined for an unset AsyncLocalStorage instance - #44166

Open
robobun wants to merge 1 commit into
mainfrom
robobun/6c050347/bake-response-outside-dev-server
Open

robobun wants to merge 1 commit into
mainfrom
robobun/6c050347/bake-response-outside-dev-server

Conversation

@robobun

@robobun robobun commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • Response.redirect(), Response.render() and new Response(<jsx />) of "bun:app" crash the process outside the dev server. A release build prints panic(main thread): Segmentation fault at address 0x0. A debug build prints panic: Expected an exception to be thrown.
  • Cause: BakeAdditionsToGlobalObject::getAsyncLocalStorage (src/jsc/bindings/BakeAdditionsToGlobalObject.h:89) returns an empty JSValue when the dev server did not set the instance. VirtualMachine::get_dev_server_async_local_storage reads that value as a thrown exception, and no exception exists.

Fix

  • The getter returns undefined when the instance is not set.
  • Correct because the two callers already handle that value. The Rust caller maps it to None. wrapComponent in BakeSSRResponse.ts tests it for truth.
  • Response.redirect() now returns a plain Response. Response.render() throws Response.render() is only available in the Bun dev server.
  • Verified: test/bake/dev/response-to-bake-response.test.ts (3 new tests, each crashes without the fix). Also react-response.test.ts for the dev server.

Background

  • "bun:app" exports the Response class of Bake. It extends the global class with render() and with JSX bodies.
  • The dev server stores an AsyncLocalStorage instance on the global object. The three calls read it for the streaming mode.
  • An empty JSValue is the marker for "the host call threw". It is not a JavaScript value.
  • Considered: remove from_js_host_call from the Rust caller. The JavaScript caller then still gets the empty value. Both callers read through the getter.

Downsides

  • The getter has one more branch. Release .text section: +0 B. The four functions that hold the getter grow by 56 B in total.
  • No other cost found. Checked both callers and the dev server suite.
Notes

Reproduction

cat > redirect.ts <<'EOF'
import { Response } from "bun:app";
console.log(Response.redirect("/login").status);
EOF
bun redirect.ts

Before: panic(main thread): Segmentation fault at address 0x0, exit code 139 (1.4.3-canary.1+367d939d9). After: prints 302.

No bundler is necessary for the crash. bun build --server-components and bun build --app rewrite the global Response of a server-side file to this class, so built code reaches the same calls.

How this was found

No user report exists. A review of #44167, which is on top of this PR and repairs the bun build --server-components output, ran Response.redirect() in the built output.

Why the empty value crashes

from_js_host_call has the contract "the value is empty if and only if an exception was thrown". get_dev_server_async_local_storage returns Err for the empty value. The host function then returns the empty value to JavaScriptCore with no exception set. Debug and ASAN builds assert on that. Release builds continue with the empty value.

Only the dev server sets the instance (src/runtime/bake/DevServer.rs, through bakeSetAsyncLocalStorage).

Not changed here

  • The dev server serves Response.redirect(url, status) as a render for each status that is not 302. A page that returns Response.redirect("/other", 301) gets HTTP 200 with the content of /other and no Location header (also 307 and 308). src/runtime/bake/hmr-runtime-server.ts tells a render from a redirect with resp.status !== 302. No PR has this yet.
  • x instanceof Response is false in a server component when x comes from fetch(), Response.json(), Response.error() or clone(), and Bun.inspect(new Response("x")) prints <null /> for the bun:app class. The cause is in src/jsc/bindings/JSBakeResponse.cpp. No PR has this yet.

Measurement

Release builds (--profile=release) of main a4f1429 and of this change, from one checkout path.

  • .text section: 58,143,477 B in both (llvm-size -A).
  • Function sizes (llvm-nm --print-size): BakeResponseClass__constructForSSR 388 -> 408 B, BakeResponseClass__constructRedirect 661 -> 673 B, BakeResponseClass__constructRender 979 -> 991 B, jsFunctionBakeGetAsyncLocalStorage 13 -> 25 B. No other function changes size.

Suites run on the debug build

  • test/bake/dev/response-to-bake-response.test.ts: 8 pass. The same file with BUN_JSC_validateExceptionChecks=1 and LeakSanitizer, as the ASAN lane runs it: 8 pass.
  • test/bake/dev/react-response.test.ts: 11 pass.

…v server did not set it

Only the dev server sets the AsyncLocalStorage instance of the bake
additions. Outside the dev server the getter returned an empty JSValue.
The Rust caller reads an empty value as a thrown exception, and the
builtin that wraps a JSX component got the empty value in JavaScript.

`Response.redirect()`, `Response.render()` and `new Response(<jsx />)`
of "bun:app" crashed the process outside the dev server. The fallbacks
in BakeResponse.rs and BakeSSRResponse.ts already handle an instance
that is not set. Return `undefined` from the getter so that they run.
@robobun

robobun commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator Author

Status: ready for review.

Reproduced on 1.4.3-canary.1+367d939d9 (release) and on a debug build of main a4f1429:

cat > redirect.ts <<'EOS'
import { Response } from "bun:app";
console.log(Response.redirect("/login").status);
EOS
bun redirect.ts
  • Release build: panic(main thread): Segmentation fault at address 0x0, exit code 139.
  • Debug build: panic: Expected an exception to be thrown, exit code 134.
  • Response.render("/404") and new Response(<jsx />) crash in the same way.
  • With this change the script prints 302.

#44167 is on top of this PR.

@robobun
robobun requested a review from alii September 28, 2026 19:36
@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: oven-sh/bun/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 7390e473-823b-424d-9e88-2e18eb738f90

📥 Commits

Reviewing files that changed from the base of the PR and between a4f1429 and db037a0.

📒 Files selected for processing (2)
  • src/jsc/bindings/BakeAdditionsToGlobalObject.h
  • test/bake/dev/response-to-bake-response.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.


Walkthrough

The async-local-storage getter now returns undefined when no instance is set. New subprocess tests check bun:app’s Response behavior outside the dev server.

Changes

Response behavior outside the dev server

Layer / File(s) Summary
Unset storage and Response behavior
src/jsc/bindings/BakeAdditionsToGlobalObject.h, test/bake/dev/response-to-bake-response.test.ts
The getter returns undefined when async-local storage is unset. Subprocess tests check redirect status and location, the Response.render() error, and JSX-like element handling by the constructor.

Suggested reviewers: jarred-sumner

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to db037

The change appears mergeable after normal checks; no actionable risk remains from this review.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly identifies the primary change: returning undefined when the AsyncLocalStorage instance is unset.
Description check ✅ Passed The description explains the problem, root cause, fix, verification steps, test results, and known limitations. It does not use the exact template headings, but it provides the required information in…

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

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

Code review found no issues

No high-confidence issues detected in this change.

@robobun

robobun commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator Author

@robobun wake up!!

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

@robobun wake up!!

@robobun

robobun commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator Author

I am here. Build #121330 passed, and the review found no issues. This PR is ready for a maintainer review. #44167 is on top of it.

This branch has not been deployed

No deployments
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.

2 participants