Skip to content

bun bake: listen on localhost when the export's unix is null, false or empty - #46717

Open
robobun wants to merge 2 commits into
mainfrom
robobun/52c6767f/bake-unix-guard
Open

robobun wants to merge 2 commits into
mainfrom
robobun/52c6767f/bake-unix-guard

Conversation

@robobun

@robobun robobun commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • bun bake listens on every interface when the app/server.ts export has a unix that Bun.serve() does not use: null, false or "". The banner still says http://localhost:3000/.
  • Cause: start() takes the socket branch for unix !== undefined (bake.ts:459) and hides the export's hostname and port. Bun.serve() then reads unix again with get_stringish (ServerConfig.rs:1057), finds none, and binds TCP with no hostname.

Fix

  • Take the socket branch only for a unix that Bun.serve() uses. Pass that value as an own unix option, so Bun.serve() does not read the export again.
  • Verified: 7 new tests in test/cli/bake/bake.test.ts. Release build of main: 87 pass, 5 fail. This branch: 92 pass, 0 fail.
  • Self-reviewed: 33 concerns raised on a larger first version, which this narrow guard replaces. One that applies here is not addressed (Notes).

Background

  • bun bake calls Bun.serve() with an object that inherits from the export, so Bun.serve() reads every option bake does not define itself.
  • For get_stringish, which reads unix, hostname and host, undefined, null, false and "" are no value. 477ec6f (bun bake: build plugins from bake.config.ts #45828) is the same guard for an empty hostname.
  • Considered the larger change that reads every address option that way (Notes). @alii: does the reworked bun bake keep unix, hostname and port in app/server.ts?

Downsides

  • A value whose string is empty still reaches the wildcard (unix: [], new String(""), the same hostname shapes): 20 of 91 probed exports, 31 before.
  • internal/bake.js and the release binary's text grow 95 bytes. A socket export makes 8 Object.defineProperty calls per attempt, not 7.
  • Beyond the symptom: a class export whose get unix() reads a #private field now serves (main throws), and Object.prototype.unix now makes a socket, not a wildcard listener.
Notes

How it was found. In review of #44013, which adds fd beside unix in the same expression. That PR's body says this bug is reported apart from it. No user reported it: the tracker has no issue, and a stable build runs bun bake only with BUN_FEATURE_FLAG_EXPERIMENTAL_BAKE=1.

Reproduce on 1.4.4 (release, Linux x64)

  1. app/server.ts: export default { unix: null, fetch: (_request, server) => Response.json(server.address) };
  2. Run BUN_FEATURE_FLAG_EXPERIMENTAL_BAKE=1 PORT=0 bun bake, with no --port and no --host.
  3. Read the url: http://localhost:N/ line, then ss -ltnp | grep "pid=<pid>,".

The listener is *:N, and a fetch of the URL answers {"address":"::"}. With this change it is [::1]:N and ::1. The same holds for unix: "" and unix: false. A realistic shape is { unix: process.env.SOCK ?? "", hostname: "127.0.0.1" } with SOCK unset: on 1.4.4 it binds *:3000 and answers on the machine's LAN address.

The larger change, held back. Read unix, hostname and host the way Bun.serve() reads them, and pass each as an own option (+153 lines, commit efeb1e7f0a on robobun/52c6767f/bake-address-once). It takes the wildcard rows from 31 of 91 to 4, fixes unix: [] and hostname: [], and stops Bun.serve() reading any address option from the export. It costs a copy of get_stringish and of the lookup that stops before Object.prototype, in JS, and +783 bytes. It is held back because bun bake is being reworked (the five PRs closed on 2026-10-21) and ServerFile in the design repo has no address option. If the keys stay, I will send it.

Measured (release builds of a8fcd319be and of this branch, Linux x64)

  • Export shapes that bind the wildcard: 31 of 91 before, 20 after. The probe runs real app/server.ts files through the real CLI and reads ss and server.address. Of the 14 shapes in the report: 8 before, 3 after.
  • The 20 that remain either hold a value whose string is empty, or name 0.0.0.0 themselves (hostname: "0.0.0.0", hostname: "0").
  • Embedded internal/bake.js: 14,339 to 14,434 bytes (wc -c). Release binary text: 107,667,648 to 107,667,743 (size). data, bss and the file size are equal.
  • Object.defineProperty per Bun.serve() attempt, from a preload hook: plain export 7 before and after, socket export 7 before and 8 after.
  • Reads of the export's unix per start with a socket export, from a getter: 2 before, 1 after. hostname, host and port are 1 each, before and after.
  • start(): 443 bytecode instructions before, 451 after (BUN_JSC_dumpGeneratedBytecodes).

Tests. Five fail on main: unix: null, unix: "", unix: false, unix: "" with hostname: "127.0.0.1", and the read count. Two pin behaviour this change keeps: --port and --host still come before the export's socket. Each clause of the fix was deleted in turn, and each deletion fails at least one test. The address rows assert server.address from a request, not the banner: the banner says localhost for a wildcard listener.

Not addressed from the self-review: the existing test bun bake listens on localhost when the export of app/server.ts has an empty hostname asserts the hostname of the banner, so it cannot see a wildcard listener. It is the test of another guard, and this change leaves it as it is.

Debug+ASAN with --timeout 90000: 91 pass, 1 fail. That one failure, bun --watch-kill-signal ... bake --check sends that signal before a restart, also times out with main's bake.ts on this machine. Also ran bun-serve-args.test.ts and bun-serve-html-entry.test.ts: 81 pass, 0 fail.

Sites of the same class, not in this change

  • A unix, hostname or host whose string is empty. The larger change above covers these.
  • websocket is read by bun bake (bake.ts:369) and again by Bun.serve() (ServerConfig.rs:989). It fails closed and sets no address.
  • The first port of the retry is derived in serve_cli.ts:113 and again in ServerConfig.rs:618. html dev server: keep an explicit port, warn when the default port moves #42020 owns that line.
  • node:http: server.listen({ path: "" }) listens on ::, where Node throws ERR_INVALID_ARG_VALUE. Its fix is Node's throw, not this one read.
  • Other in-tree Bun.serve({ unix }) callers are handled separately.

Also unchanged: a socket path that is in use (a stale file too) is tried six times by serveOnFreePort (serve_cli.ts:112), then bun bake prints the EADDRINUSE error and waits for an edit. Main does the same. The wait is the rule of #45828 for an address that the export gives.

Order against #44013. That PR renames this expression to useSocket and adds fd. Either can land first. Merging this branch into it leaves one conflict region, the predicate. The resolution is const useSocket = (hasUnix || exported?.fd != null) && flags.hostname === undefined && flags.port === undefined, plus own("fd", exportedFd) beside own("unix", exportedUnix).


no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/cli/bake/bake.test.ts

…r empty

`start()` took the socket branch for every `unix` of the `app/server.ts`
export that was not `undefined`. That branch hides the export's `hostname`,
`host` and `port` and sets no "localhost" default. `Bun.serve()` reads `null`,
`false` and an empty string as no socket, so the dev server listened on every
interface, on port 3000.

The socket branch is now taken only for a value that `Bun.serve()` uses. The
value that was tested is passed as an own `unix` option, so `Bun.serve()` does
not read the export's `unix` a second time.
@robobun

robobun commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 5:15 AM PT - Oct 24th, 2026

❌ @robobun, your commit 8faa6dc has 2 failures in Build #131862 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 46717

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

bun-46717 --bun

@robobun

robobun commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

  • Reproduced on released 1.4.4 (Linux x64): an app/server.ts that exports unix: null, unix: "" or unix: false makes bun bake listen on *:N, and a request to the printed URL answers {"address":"::"}. ss -ltnp confirms the wildcard listener. The export's own hostname and port are dropped in the same case.
  • The fix takes the socket branch only for a unix value that Bun.serve() uses, and passes that value as an own option so Bun.serve() does not read the export a second time.
  • 7 new tests in test/cli/bake/bake.test.ts. Five fail on main, two pin behaviour this change keeps. Each clause of the fix was deleted in turn, and every deletion fails at least one test.
  • CI, build 131862 (finished): the 7 new tests pass on every lane: Linux, macOS aarch64, macOS x64 and Windows.
  • Two jobs are red, and this diff touches neither. One is test/js/first_party/ws/ws-proxy.test.ts on Windows 2019 x64, which is also red in the final builds of recently merged PRs. The other is a macOS aarch64 shard whose artifact download timed out before a test ran.
  • On macOS x64, one existing row of bake.test.ts (bun bake rejects a framework with clientPages and no clientEntryPoint) timed out once and passed on the retry. A row of the same table needs a retry on that lane in builds of other branches too (131882, 131806).

@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 5e18294 and 89a7388.

Files selected for processing: 2
  • src/js/internal/bake.ts
  • test/cli/bake/bake.test.ts

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

Walkthrough

bun bake now treats unix as active only for values other than null, false, or an empty string. It uses the Unix socket only when neither a hostname nor a port flag is specified, and passes the selected value to Bun.serve().

Changes

Bake listener selection

Layer / File(s) Summary
Select listener and validate serving behavior
src/js/internal/bake.ts, test/cli/bake/bake.test.ts
bun bake reads the exported unix value once and selects the Unix socket only when it is non-falsy and neither hostname nor port is specified. Tests cover falsy values, getter reads, and TCP serving when either flag is supplied.

Suggested reviewers: alii

Priority: Normal

Merge Risk: Minimal · up to 89a73

The listener-selection change is mergeable after normal checks; no actionable issue remains identified.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: bun bake uses localhost when the exported unix value is null, false, or empty.
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.
Description check Passed The description is complete and relevant. It explains the problem, cause, fix, scope, verification results, tests, limitations, and remaining issues. Although it does not use the exact template headin…
Full details: Description check

Explanation

The description is complete and relevant. It explains the problem, cause, fix, scope, verification results, tests, limitations, and remaining issues. Although it does not use the exact template headings, it provides the required information in equivalent sections.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟣 src/js/internal/bake.ts — Developers with a stale socket file get six identical bind attempts on the same path, then an uncaught error with a stack trace instead of bake's one-line error. In the useUnix branch the port argument to serveOnFreePort is undefined (src/js/internal/bake.ts:491) and options() ignores the port it is handed (bake.ts:477-482), yet a unix listen failure carries code EADDRINUSE (src/runtime/server/mod.rs:724), so serve_cli.ts:112-124 retries five more times with the same socket. Fix: skip the port retry loop when the options use a unix socket, and route the final listen error through BakeError so bun bake prints error: ... and exits as it does for a TCP address.

    Why this was flagged

    Trigger: app/server.ts exports unix: "./app.sock" and the file exists from a previous run killed without cleanup; the developer runs bun bake with no --host/--port. Entry: start() calls serveOnFreePort at src/js/internal/bake.ts:465 with port undefined (bake.ts:491). Bun.serve() fails to listen on the socket and throws a SystemError whose code is EADDRINUSE (src/runtime/server/mod.rs:719-728). serveOnFreePort treats that as a busy TCP port (src/js/internal/serve_cli.ts:112) and calls Bun.serve(withId(defaultPort++)) five more times, but options() in the useUnix branch (bake.ts:477-482) defines port as undefined and keeps the same unix, so every retry binds the same path. After the sixth failure the error reaches the catch at bake.ts:493; addressFromExport is true because useUnix is true (bake.ts:498-499), so the raw error is rethrown (bake.ts:504) and main() rethrows non-BakeErrors (bake.ts:522): the process dies with an unhandled error and stack trace rather than the error: line and exit code the TCP path gives. The base branch behaves the same.

    Verification: pre-existing. Trigger: app/server.ts exports a non-empty unix string, no --host/--port, and the socket path is already bound or a stale file from a killed run. The port argument is undefined (src/js/internal/bake.ts:491) and the options factory ignores port in the useUnix branch (bake.ts:477-482), so every attempt yields the same socket path. The base commit has byte-identical behavior on this exact path.

Comment thread test/cli/bake/bake.test.ts Resolved
Comment thread test/cli/bake/bake.test.ts Resolved
Comment thread src/js/internal/bake.ts Resolved
…ugh spawnBake

With `--port` or `--host`, `bun bake` makes no socket for the `unix` of the
export, so the test needs no POSIX-only socket path. `spawnBake` takes extra
environment variables, and the new tests use it in place of their own
`Bun.spawn` call.
@robobun

robobun commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Reply to the additional finding in the review (a unix socket path that is in use):

Confirmed, and main behaves the same. With a stale s.sock, bun bake calls Bun.serve() six times with the same path and prints EADDRINUSE: address already in use, listen '<path>' with a stack. It does not exit. It waits for an edit of app/server.ts: the address comes from the export, and #45828 made that case wait.

Not changed in this PR. The retry is in serveOnFreePort (src/js/internal/serve_cli.ts:112), which bun ./index.html shares and #42020 edits. This PR changes only which unix values take the socket branch. The case is now in the PR notes.

The two test nits are done in 8faa6dc, and the three threads have replies.

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

LGTM — the second commit addressed both test nits, and the fix itself is small and matches what Bun.serve() does.

What was reviewed:

  • hasUnix at src/js/internal/bake.ts:460 against get_stringish (src/jsc/JSValue.rs:1141) and the unix read in src/runtime/server/ServerConfig.rs:1057: undefined/null/false/"" are no socket on both sides; 0, NaN and symbols reach Bun.serve() the same way as before.
  • The own unix write at bake.ts:482 is the only other reader of the export's unix, so the getter is read exactly once; serveOnFreePort does not re-read it.
  • Error classification at bake.ts:498: with unix: null and no flags, addressFromExport is now false, so a listen failure on the default address is correctly non-fixable.
  • New tests: PORT=0 is read by ServerConfig.rs:619, server.address is the DTO from SocketAddress::into_dto with an address field, and the rows assert the bound address rather than the banner.
Extended reasoning...

The change touches start() in the builtin src/js/internal/bake.ts (six lines: read unix once, a presence predicate matching Bun.serve's get_stringish, and an own unix property in the socket branch) plus seven new subprocess tests in test/cli/bake/bake.test.ts. It narrows an accidental wildcard listen for unix: null/false/"" to loopback; no auth, crypto or injection surface is involved, and bun bake is gated behind an experimental feature flag. Neither path has a CODEOWNER, the hunt ran dry with no findings, and the second commit resolved both inline nits from the prior review. The remaining gap for non-primitive values whose string form is empty was already noted as pre-existing and the author documents it as deliberately held back.

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