Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one
by oven-shRust
Last 12 weeks · 1574 commits
5 of 6 standards met
What does this PR do? Fixes #12157. Fixes #24474. was a direct alias for , so none of the methods (, , , ) were available on servers created via . In Node.js, extends , which provides these. Reproduction Fix is now its own class extending (so is still , and does not gain these methods). accepts the same options as the constructor (key/cert/ca, pfx, minVersion/maxVersion, secureProtocol, ciphers, per-context requestCert/rejectUnauthorized; an empty hostname throws like Node) and buffers the context. When is called, the buffered SNI contexts are passed to via the array so each is matched via SNI. When called after , a new native binding () registers the SNI context on the running uWS SSL app via and reinstalls routes for that domain. rebuilds the default TLS config used at from the options given (an omitted option is cleared, as in Node), keeping the server's /; like Node, providing alone does not start requiring a client certificate. Nothing is applied unless every option validated. The constructor, and share one option pipeline ( in , extracted from 's constructor), so the three cannot drift apart again. marks itself TLS before runs, so it gets a real config even when constructed without certificates. / are stubbed the same way as on . Also fixed the existing typo in the stubs. Verification New tests at : exposes all four methods and is an subclass before : connecting with SNI and returns the respective per-hostname certs (CN / ); an unknown SNI falls through to the default cert (CN ) after : same SNI behavior on a running server, plus an HTTPS request still reaches the request handler before replaces the default cert on a server created with no initial TLS options does not require a client certificate throws before and after , and works through (both phases) and applies , clears a the constructor had set, rejects an unknown without applying anything, and keeps the constructor's client certificate policy (a client with a certificate from the configured CA is served the new certificate, one without is refused) All tests fail on () and pass with this change. Existing , , , and the Node parallel tests still pass. Supersedes #31095 by @sam-shridhar1950f, which introduced the same subclass split with method stubs (credited with a trailer on the commit); this PR additionally wires to the underlying SNI mechanism, both at listen time and on a running server, so the contexts actually select per-hostname certificates. Making a distinct class also fixes the confusion from #31125 (supertest [review] gate passed · iteration 11 · 14 files touched fails on main (without fix) passes on PR (with fix) diff hotspot gate history · 3 passed · 0 rejected · iteration 11 evidence per changed file
Problem Subshell redirections like were rejected at parse time with: This affected real packages like that use subshell redirections in install scripts. Root Cause The shell parser (, ) explicitly rejected subshells with non-empty , even though the AST already carries and on . The runtime state () spawned its inner directly and never consumed those fields (the code carried a TODO for exactly this). Fix 1. Remove the parser guard rejecting subshell redirections 2. Rework the state machine: now expands the redirect target via an child ( state) before transitioning to exec 3. Add : opens the redirect target (via / ) and rewires stdin/stdout/stderr with /, mirroring ; fd-dup forms (, ) rewire without opening a file 4. Add : sets , enqueues the message on the Subshell stderr writer (the dispatch already existed), so redirect failures propagate non-zero exit 5. collects the expanded path from the child and re-enters JS-object redirects () remain rejected at runtime with a clear error: the Subshell type has no blob/buffer slots (that path is Cmd-specific via ). This PR was originally implemented in Zig and re-ported to Rust after the Rust rewrite landed on main. Verification New tests in (subshell describe block): stdout redirect, append redirect, variable-expanded path, plus the previously-disabled "redirection on subshell" ported case now asserts real output. All fail on the released bun (parser rejects) and pass on this branch. Closes #11124 Rebase notes (post shell-API refactor on main) Main refactored the shell interpreter while this PR was open; the rebase resolved: lost its arg; lost its / args — call sites updated and its dispatch arm had been dropped from (nothing used them on main); re-added for the Subshell failing-error path / became in ; made again since the runtime now consumes them was renamed Adopted main's handling for the redirect (honors the value computed by on POSIX), replacing the old hard-coded platform constant no test proof** · iteration 17 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/shell/bunshell.test.ts
Problem On a long-lived session, a stream whose final frame is written from the outbound queue is never released: its object stays reachable (a to it never clears) and its native slot and engine entry stay in the session's maps until the whole session is torn down. Node releases such streams as soon as they complete. A frame goes through the outbound queue whenever it cannot be written immediately: the peer's stream window is used up (any response or request body larger than the window, 64 KiB by default), the socket has backpressure, or any other stream on the session already has frames queued. In that last case even a two-byte response leaks while one slow stream is stalled, so on a busy server this is one leaked stream per request, for every request answered after the request body was consumed. Cause: () moves the stream from to when its last queued frame has been written and dispatches , but never calls . Every other place that sends the local does ('s direct-write close at , at , at ), and () covers the opposite order, where the peer's arrives last. Nothing else releases the stream on this path: the JS side deliberately skips the host call for a stream that closed natively (), and even when that call is made, returns early for a stream that is already . Fix Call in the arm of , mirroring 's direct-write close (same order: identifier captured, state set, resources freed, dispatched). The arm sits in the tail shared by both of 's dequeue branches, so it covers END_STREAM riding on the last body frame as well as on an empty frame queued by itself ( without a body, , and therefore every compat-API response, which always ends on an empty frame after the body). Correct because a stream in that arm is fully closed: the peer's half was already closed () and the frame just written carried our , which is exactly the condition under which the direct-write path frees the stream today. The result matches node, which drops a stream when both halves close regardless of how its last frame was written. Safe at this call site: only drops the stream's JS roots, drops its abort-signal listener, and pushes the id onto ; the box and engine entry are evicted on the next inbound batch at dispatch depth 0, as for every other caller. The stream's own queue is empty at this point, so no write callbacks are dropped. holds a keepalive across , and the iterator re-looks up ids on each step, the same situation / already free streams in. The arm is unchanged: frees that stream when the peer's arrives, as it does for the direct-write path. Independent of #34675 (which merges the stream's two JS roots into one): its hunk only changes how the identifier is looked up, so this arm needs the release with or without it. Tests: , , five cases driven through one real client/server session with s on the streams: a response larger than the client's advertised window (server stream, END_STREAM on the last body frame); responses queued behind another stream's stalled response, as , as without a body and through the compat API (server streams; the last two end on an empty queued frame); and a request body larger than the server's window when the server answered before the body finished (client stream). Each case also asserts the premise it relies on (the tail was still queued when the response was written or arrived and was then delivered in full, or the stalled response was still pending at the end), so a case that stopped exercising the queue would fail rather than pass vacuously. All five report 4/4 streams still alive on the unfixed build and 0/4 with the fix. Also run on the debug build: the full ; (all tests pass; four subprocess-spawning cases in the "DATA payload survives its ArrayBuffer being detached" block only pass with a raised timeout here because each debug subprocess needs ~3 s to load , which is independent of this change); the 261 upstream node tests (all pass); and , the grpc-js tests, and the http2 regression tests under (all pass). Background in Bun has two native layers per session: the inbound engine (), which parses incoming frames and keeps one entry per stream, and the older outbound encoder in , which keeps a per stream in and also holds the stream's JS object rooted (a handle, so the GC cannot collect it) in and in the session-level map. The engine never sees outbound frames, so it cannot tell when the local half of a stream closes. is the bridge: it drops the JS roots and queues the id on ; drains that queue before the next inbound batch, removing the engine entry and freeing the box. A completed stream that never reaches therefore keeps all three things alive for the life of the session. Outbound queue: writes frames straight to the socket while the peer's flow-control window allows it. Once it cannot (window exhausted, socket backpressure, or the session already has queued frames, which must keep their order), the remaining frames are queued per stream and writes them later, typically from the handler or the socket's writable callback. is the per-stream step of that drain, so the stream's close tail runs there instead of in whenever the final frame was queued. / / are RFC 9113 stream states: the peer has finished sending, we have finished sending, or both. A stream is complete, and can be released, once it reaches . Standalone repro (20 sequential GETs on one session, then GC) The client-side counterpart (server answers as soon as the headers arrive while a 200 KB request body is still queued on the client) behaves the same way: 20/20 client streams alive on 1.4.0, 0/20 with the fix, 0/20 on node.
Repository: oven-sh/bun. Description: Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one Stars: 95396, Forks: 4962. Primary language: Rust. Languages: Rust (65%), C++ (20.9%), TypeScript (8.9%), C (2.7%), JavaScript (1.7%). Homepage: https://bun.com Topics: bun, bundler, javascript, javascriptcore, jsx, nodejs, npm, react, rust, rust-lang, transpiler, typescript. Latest release: bun-v1.3.14 (3mo ago). Open PRs: 100, open issues: 7673. Last activity: 1m ago. Community health: 100%. Top contributors: Jarred-Sumner, robobun, dylan-conway, nektro, cirospaciari, paperclover, Electroid, alii, colinhacks, pfgithub and others.