Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
144 commits
Select commit Hold shift + click to select a range
aaaa057
node:http/https/http2: raise Node v26.3.0 compat to ~93% and sync the…
cirospaciari Jun 22, 2026
8d1e6a3
node:http: post-pad the request trailer section before the 8-byte fie…
cirospaciari Jun 26, 2026
aafedba
node:http: add a standalone server-timeout-knobs test
cirospaciari Jun 29, 2026
92050fb
test: keep the huge-ArrayBuffer structuredClone cases off the OOM killer
cirospaciari Jun 29, 2026
18c1c46
node:http2: restore a client stream's captured empty async context
cirospaciari Jun 29, 2026
3946ff4
test: quarantine test-net-connect-memleak on linux-x64-musl
cirospaciari Jun 29, 2026
738780e
usockets: don't truncate a hung-up socket's stream at the poll's EOF …
cirospaciari Jun 29, 2026
c0ce5fe
node:http: do not shut the socket down while the response is still fl…
cirospaciari Jun 30, 2026
3946946
Merge origin/main into claude/node-http-http2-compat
cirospaciari Jul 1, 2026
aaaef71
Merge origin/main into claude/node-http-http2-compat
cirospaciari Jul 6, 2026
6961bb8
node:http2: detach the allowHTTP1 fallback socket when a response fin…
cirospaciari Jul 6, 2026
8b4066c
node:http2: refuse over-budget streams with ENHANCE_YOUR_CALM like node
cirospaciari Jul 6, 2026
c2a68b3
node:http2: treat late frames on evicted locally-initiated streams as…
cirospaciari Jul 6, 2026
d43eb06
Merge origin/main into claude/node-http-http2-compat
cirospaciari Jul 7, 2026
178c628
node:http: fix trailer/EOF parser hangs, leaks and Windows fatal-writ…
alii Jul 7, 2026
9129c09
node:http/http2: fix Node-compat divergences from review
alii Jul 7, 2026
93063de
node:http/http2: dedup ERR_INVALID_ARG_TYPE formatter and clarify ser…
alii Jul 7, 2026
9ab92d4
node:http: trailer/domain coverage + review-fix cleanups
alii Jul 7, 2026
5e249f0
node:http2: don't gate client close() on receiving the peer's SETTING…
alii Jul 7, 2026
94255ac
node:http: honor server.httpAllowHalfOpen assigned after listen()
cirospaciari Jul 7, 2026
b7b7572
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 7, 2026
a390358
node:http: allow the Trailer response header on chunked responses
cirospaciari Jul 7, 2026
4a8adab
node:http: capture request trailers per request, not per connection
cirospaciari Jul 7, 2026
4d7f32f
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 7, 2026
8e7d70c
node:http/http2: fix pipelined 1xx ordering, socket.parser at 'connec…
alii Jul 7, 2026
99eac9e
node:http2: reopen receive window from _read() and clear kOwnsFd in r…
alii Jul 8, 2026
30392cc
node:http2: pair the receive-window reopen with Node's readStop-on-fu…
cirospaciari Jul 8, 2026
f2d6f54
node:http: move node-compat per-connection state out of Bun.serve's H…
cirospaciari Jul 8, 2026
b0433f5
node:http: compile the node-compat state and handlers out of Bun.serv…
cirospaciari Jul 8, 2026
e12dfb7
node:http2: close a finished server stream whose request body was nev…
cirospaciari Jul 8, 2026
bb67830
node:http2: only auto-close an unread request when its body is actual…
cirospaciari Jul 8, 2026
c14281e
node:http2: make the unread-request close race-free; node:http fixes …
cirospaciari Jul 9, 2026
bd8fc38
node:http2: run the settings() callback before emitting 'localSettings'
cirospaciari Jul 9, 2026
d93f3d9
node:http2: let a drained request finish before a graceful session de…
cirospaciari Jul 9, 2026
67f943d
node:http2: flush settings() and give its ACK a bounded grace on close
cirospaciari Jul 9, 2026
252aebb
node:http: fail a request whose trailer section is malformed; honor i…
cirospaciari Jul 9, 2026
23965a8
node:http: never dispatch a pipelined request behind Connection: close
cirospaciari Jul 9, 2026
46a59e0
Merge remote-tracking branch 'origin/main' into ciro/ci-70030-fix
cirospaciari Jul 9, 2026
7095981
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 9, 2026
7a2fe94
document the trailer-validation capacity bound
cirospaciari Jul 9, 2026
269c81a
node:http: server hot-path performance (stacked on #32488) (#33879)
cirospaciari Jul 10, 2026
d49cff1
Merge origin/main into claude/node-http-http2-compat
cirospaciari Jul 10, 2026
5fb4a01
Merge branch 'main' into claude/node-http-http2-compat
cirospaciari Jul 10, 2026
d1dbe3e
Merge branch 'main' into claude/node-http-http2-compat
cirospaciari Jul 11, 2026
74b1081
uws: fold the per-response booleans into the flags word, drop the nod…
cirospaciari Jul 11, 2026
cceac70
paths: return the resolved slice directly
cirospaciari Jul 11, 2026
c2ce18d
test: stop skipping the HTTP/2 test suite
Jarred-Sumner Jul 11, 2026
06e9db8
runtime: build the body-value pool on the heap instead of a stack tem…
cirospaciari Jul 11, 2026
3680876
Merge remote-tracking branch 'origin/main' into claude/node-http-http…
cirospaciari Jul 11, 2026
bc0b464
test: drop vendored http2 tests that depend on Node-internal machinery
cirospaciari Jul 11, 2026
d5ea586
uws: gate the leading-empty-line tolerance behind the node-http parser
cirospaciari Jul 11, 2026
0bd6587
node:http: drop the misleading optional chain on the pipelined queued…
cirospaciari Jul 11, 2026
9a53587
node:http2: ignore ECONNRESET once a GOAWAY has been received
cirospaciari Jul 11, 2026
031ab0b
node:http2: release the parser refs that process.exit() strands
cirospaciari Jul 11, 2026
072d922
worker: release the loop state a terminated worker leaves behind
cirospaciari Jul 13, 2026
79d630d
test: stop adding expectations.txt entries; gate the named-pipe tests…
cirospaciari Jul 13, 2026
dd56a89
uws: drop the last runtime node-http-compat plumbing and the maybe_un…
cirospaciari Jul 13, 2026
9dbfb99
node:http: bound pipelined response buffering; match Node on a missin…
cirospaciari Jul 13, 2026
deb66a0
Merge remote-tracking branch 'origin/main' into claude/node-http-http…
cirospaciari Jul 13, 2026
4d8585e
node:http: report a missing chunk terminator as HPE_STRICT, like llhttp
cirospaciari Jul 13, 2026
a61111d
Fix clippy let_and_return in the h2 write path
cirospaciari Jul 13, 2026
45480cf
node:https: deliver the TLS EOF to the HTTP layer instead of force-cl…
cirospaciari Jul 13, 2026
bd10ee6
node:http2: resume the socket on graceful session close, like Node
cirospaciari Jul 13, 2026
08e091b
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 13, 2026
116577c
node:http2: judge session idleness by a monotonic written counter, li…
cirospaciari Jul 13, 2026
4bee8a2
node:http2: tear the transport down when a native write fails fatally
cirospaciari Jul 13, 2026
adaa711
webcore: release the FileSink refs that process exit strands
cirospaciari Jul 13, 2026
8ef31c8
node:http: count header bytes in a queued pipelined response's output
cirospaciari Jul 13, 2026
d7bfa55
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 13, 2026
9c17084
node:http2: read session write progress from the native handle's counter
cirospaciari Jul 13, 2026
0d1e01e
node:http2: end the socket first and destroy a tick later on error te…
cirospaciari Jul 13, 2026
2d4a23f
test: calibrate the ASAN leak tests for sanitizer overhead; patch mim…
cirospaciari Jul 13, 2026
b4fa757
usockets: retry macOS's racy EPROTOTYPE like libuv instead of reporti…
cirospaciari Jul 13, 2026
8061d7f
usockets: don't resume a paused socket's reads when re-arming for a w…
cirospaciari Jul 13, 2026
07be335
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 13, 2026
f636e4a
node:http2: don't detach the JS wrapper when closing a fatally-failed…
cirospaciari Jul 13, 2026
5cecac8
usockets(windows): request UV_DISCONNECT so a peer reset fires with n…
cirospaciari Jul 13, 2026
e67ff52
node:http2: make session destroy idempotent, like Node
cirospaciari Jul 14, 2026
f61faf4
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 14, 2026
246c1dc
test: staged half-close-mid-upload teardown coverage
cirospaciari Jul 14, 2026
3807122
test: staged PING-flood teardown coverage
cirospaciari Jul 14, 2026
c59f9e9
usockets(windows): treat UV_DISCONNECT as the EOF hint and arm it unc…
cirospaciari Jul 14, 2026
8d03151
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 14, 2026
4b97a68
node:http2: don't latch destroy on the nothing-to-tear-down early-out
cirospaciari Jul 14, 2026
76c1a4b
node:http2: don't re-destroy an already-destroyed stream from the def…
cirospaciari Jul 14, 2026
2663f0f
node:http2: whitelist the errnos that fatally close the transport
cirospaciari Jul 14, 2026
929ac1e
usockets(windows): make UV_DISCONNECT one-shot so a deferred EOF cann…
cirospaciari Jul 14, 2026
55062db
node:http2: treat an error GOAWAY after our own graceful close as mut…
cirospaciari Jul 14, 2026
affdc52
test: pin the default-localhost loopback contract the vendored suites…
cirospaciari Jul 14, 2026
f74f866
usockets(windows): UV_DISCONNECT surfaces as readable only - recv() o…
cirospaciari Jul 14, 2026
326d048
test: give the abort fixtures measured release headroom
cirospaciari Jul 14, 2026
d5d0254
usockets(windows): map UV_DISCONNECT to EOF only for already-shut-dow…
cirospaciari Jul 14, 2026
29d1fe6
usockets(windows): sync poll_cb's header comment with the conditional…
cirospaciari Jul 14, 2026
4768dd3
test: extend the URLSearchParams leak deadline to ASAN like the debug…
cirospaciari Jul 14, 2026
b4b9de5
node:http2: only skip the deferred error sweep for listener-less dest…
cirospaciari Jul 14, 2026
8763fd9
test: event-taped twins for the two darwin-intermittent h2 scenarios
cirospaciari Jul 14, 2026
507986e
test: probe the PING-flood teardown on both loopback families
cirospaciari Jul 14, 2026
5a312c3
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 14, 2026
af4d380
test: probe the half-close teardown over the default-localhost path too
cirospaciari Jul 14, 2026
8a658d7
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 14, 2026
916afff
test: traced verbatim twin of the half-close teardown in the same run…
cirospaciari Jul 14, 2026
7f1b0f2
test: also probe the PING-flood teardown over the vendored test's exa…
cirospaciari Jul 14, 2026
49e01a3
test: harden the half-close trace and add a 127.0.0.1 sibling in the …
cirospaciari Jul 14, 2026
8905777
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 14, 2026
3db5527
test: half-close trace v3 - a ref'd ticker to test whether the loop t…
cirospaciari Jul 14, 2026
cb1b961
test: turn the v4 half-close trace into the timerless A/B control
cirospaciari Jul 14, 2026
173ca07
test: pin unref'd-timer firing under pure socket-wait
cirospaciari Jul 14, 2026
2a9a332
usockets(windows): give the libuv path a real bun_tick - always poll,…
cirospaciari Jul 14, 2026
b7e7338
usockets(windows): include time.h for struct timespec in the libuv bu…
cirospaciari Jul 14, 2026
671f12e
usockets(windows): us_loop_pump must also bypass uv_run's alive-guard
cirospaciari Jul 14, 2026
c1f6b65
server: keep the graceful-stop ref until the drain completes; revert …
cirospaciari Jul 14, 2026
349b56b
test: un-quarantine the Windows net-reset trio
cirospaciari Jul 14, 2026
662ee8c
Revert the Windows tick and graceful-stop keepalive changes
cirospaciari Jul 14, 2026
759e4b2
test: remove the wedge-investigation traced twins
cirospaciari Jul 14, 2026
0f4370e
node:http server: release request accounting on tunnel handoff; fix w…
cirospaciari Jul 14, 2026
f28abe6
server: keep the graceful-stop ref until the drain completes
cirospaciari Jul 14, 2026
12d4e41
test: traced verbatim twin of the PING-flood test
cirospaciari Jul 14, 2026
209e15c
test: tape the wire frames the raw server receives in the late-RST twin
cirospaciari Jul 14, 2026
6300e69
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 14, 2026
be17e34
Bound the retry of unclassified send() errnos and surface them as errors
cirospaciari Jul 14, 2026
d6a6e43
usockets(windows): deliver peer resets to write-only and paused polls
cirospaciari Jul 14, 2026
deb14ba
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 14, 2026
a4a3e07
fix the libuv abort patch's hunk headers (git-generated against the p…
cirospaciari Jul 14, 2026
4f94249
test: extend the flood tracer's watchdog past done with client-state …
cirospaciari Jul 14, 2026
b4b48f3
usockets(windows): a reset behind buffered data must not defer forever
cirospaciari Jul 14, 2026
bee36cb
node:http2: don't close a transport whose fatal write already drained
cirospaciari Jul 14, 2026
a5ba437
usockets(windows): the paused-probe must follow socket adoption and s…
cirospaciari Jul 14, 2026
776cb49
usockets(windows): sweep-escalate deferred FINs whose peer later reset
cirospaciari Jul 14, 2026
8939a7e
usockets(windows): observe a peer reset with a zero-byte send, not SO…
cirospaciari Jul 14, 2026
4e96ac2
usockets(windows): use raw winsock send for the reset probe (bsd_send…
cirospaciari Jul 14, 2026
312e071
Merge branch 'main' into claude/node-http-http2-compat
cirospaciari Jul 14, 2026
69ac4fd
test: retire the PING-flood tracer - its diagnosis is complete
cirospaciari Jul 14, 2026
223aca6
test: drop test-http2-ping-flood.js - vendored by this branch, unpass…
cirospaciari Jul 14, 2026
c51f8a4
test: give the stream-release check an immediate-drain per GC pass
cirospaciari Jul 14, 2026
a0e2dfb
node:net: server sockets must emit 'error' from native error dispatches
cirospaciari Jul 14, 2026
30dd839
node:net: revert the delegated-error guard bypass - the drain and rea…
cirospaciari Jul 14, 2026
eb693c9
test: stress twin for the worker transfer+terminate ASAN abort
cirospaciari Jul 14, 2026
947cef1
test: scrub upstream-literal markers from newly vendored node tests
robobun Jul 14, 2026
00eaa58
Revert "test: scrub upstream-literal markers from newly vendored node…
cirospaciari Jul 14, 2026
60bce3b
node:net: surface fatal-flush errors on server sockets; scope verify …
robobun Jul 14, 2026
79d4801
ci: retrigger
robobun Jul 15, 2026
1bb4f37
Merge main into claude/node-http-http2-compat
cirospaciari Jul 15, 2026
5dd2387
node:http: report the header duplicate-handling flag out-of-band
cirospaciari Jul 15, 2026
a4267b5
node:http: avoid re-lowercasing conventional header names on every ac…
cirospaciari Jul 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/bun-usockets/src/context.c
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ static void us_internal_init_listen_socket(struct us_listen_socket_t *ls,
s->flags.is_closed = 0;
s->flags.adopted = 0;
s->flags.allow_half_open = (options & LIBUS_SOCKET_ALLOW_HALF_OPEN);
s->unclassified_send_failures = 0;
s->next = 0;
s->prev = 0;
s->connect_state = NULL;
Expand Down Expand Up @@ -489,6 +490,7 @@ static inline void us_internal_init_connect_socket(struct us_socket_t *s,
s->flags.is_closed = 0;
s->flags.adopted = 0;
s->flags.last_write_failed = 0;
s->unclassified_send_failures = 0;
s->connect_state = NULL;
s->connect_next = NULL;
}
Expand Down Expand Up @@ -825,3 +827,10 @@ struct us_bun_verify_error_t us_socket_verify_error(struct us_socket_t *s) {
}
return (struct us_bun_verify_error_t) { .error = 0, .code = NULL, .reason = NULL };
}

const char *us_socket_sni_servername(struct us_socket_t *s) {
if (s->ssl) {
return us_internal_ssl_sni_servername(s);
}
return NULL;
}
77 changes: 72 additions & 5 deletions packages/bun-usockets/src/crypto/openssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ static int us_ssl_listener_ex_idx = -1;
static int us_ssl_is_socket_ex_idx = -1;
/* Defined in Rust (src/uws_sys/SocketKind.rs) so the ordinal tracks the enum. */
extern const unsigned char BUN_SOCKET_KIND_BUN_SOCKET_TLS;
extern const unsigned char BUN_SOCKET_KIND_UWS_HTTP_TLS;
/* Serialized resumable session parked by the new-session callback until the
* SSL stack unwinds; freed with the SSL if never delivered. */
static int us_ssl_pending_session_idx = -1;
Expand Down Expand Up @@ -1338,6 +1339,7 @@ void us_internal_ssl_attach(struct us_socket_t *s, SSL_CTX *ctx,
s->ssl_raw_tap = 0;
s->ssl_shutdown_after_spill = 0;
s->ssl_close_after_spill = 0;
s->ssl_end_delivered = 0;
s->ssl_in_use = 0;
s->ssl_pending_detach = 0;
s->ssl_pending_close_code = 0;
Expand Down Expand Up @@ -1758,12 +1760,57 @@ struct us_socket_t *us_internal_ssl_on_close(struct us_socket_t *s, int code, vo
return ret;
}

/* The EOF dispatch below is scoped to uWS HTTP server sockets: their
* context's onEnd owns the EOF (premature-EOF clientError
* HPE_INVALID_EOF_STATE, CONNECT/Upgrade half-open, pipeline drain after
* FIN), and closing without dispatching silently skipped all of it for
* node:https. Every other TLS socket kind predates the dispatch and
* synthesizes its JS 'end' from the close event, so they keep the
* historical force-close (dispatching for them strands sockets whose end
* handler expects the transport to close underneath it). */
static int ssl_wants_eof_dispatch(struct us_socket_t *s) {
return us_socket_kind(s) == BUN_SOCKET_KIND_UWS_HTTP_TLS;
}

/* Deliver the plaintext EOF to the user layer once, like the plain-TCP path
* (loop.c dispatches us_dispatch_end for non-SSL sockets). Both TLS EOF
* paths (peer close_notify -> ZERO_RETURN, and the raw TCP FIN that usually
* follows it) route through here, so the bit keeps the end handler
* single-shot. */
static struct us_socket_t *ssl_deliver_eof(struct us_socket_t *s) {
if (s->ssl_end_delivered) {
return s;
}
s->ssl_end_delivered = 1;
return us_dispatch_end(s);
}

struct us_socket_t *us_internal_ssl_on_end(struct us_socket_t *s) {
ssl_set_loop_data(s);
/* TCP FIN under TLS: the peer's write side is gone, so no close_notify reply
* is coming. Send ours best-effort and raw-close now — deferring (the
* code==0 path in ssl_close) would wait forever, and with native
* allowHalfOpen=true the loop.c caller no longer raw-closes for us. */
if (ssl_wants_eof_dispatch(s)) {
/* Raw TCP FIN under TLS: the peer's write side is gone, so no
* close_notify reply is ever coming. Record the TLS-level shutdown as
* received so a later graceful close (an allow_half_open socket ending
* its side after this EOF) completes immediately in ssl_handle_shutdown
* instead of deferring for an alert that cannot arrive. */
if (!ssl_gone(s)) {
SSL_set_shutdown(s_ssl(s), SSL_get_shutdown(s_ssl(s)) | SSL_RECEIVED_SHUTDOWN);
}
s = ssl_deliver_eof(s);
if (!s || us_socket_is_closed(s)) {
return s;
}
if (s->flags.allow_half_open) {
/* Keep the write side alive like the plain-TCP half-open branch in
* loop.c: TCP permits writing after a received FIN, so queued
* responses still flush and the app's own end() completes the
* shutdown. */
return s;
}
}
/* TCP FIN with no half-open: send our close_notify best-effort and
* raw-close now — deferring (the code==0 path in ssl_close) would wait
* forever. */
s = ssl_close(s, 0, NULL);
if (s && !us_socket_is_closed(s)) {
s = us_internal_socket_close_raw(s, LIBUS_SOCKET_CLOSE_CODE_CLEAN_SHUTDOWN, NULL);
Expand Down Expand Up @@ -1890,14 +1937,29 @@ struct us_socket_t *us_internal_ssl_on_data(struct us_socket_t *s, char *data, i
* close_notify was parked by the new-session callback; deliver it
* first (wire order - the ticket preceded these bytes, and Node's
* NewSessionCallback runs before the data reaches JS), then the
* decrypted data, then close. */
* decrypted data, then the EOF. */
ssl_flush_pending_session(s);
ssl_flush_pending_keylog(s);
if (ssl_gone(s)) return NULL;
if (read) {
s = us_dispatch_data(s, loop_ssl_data->ssl_read_output + LIBUS_RECV_BUFFER_PADDING, read);
if (!s || ssl_gone(s)) return NULL;
}
/* TLS-level EOF: for uWS HTTP sockets, dispatch the user layer's
* end handler like a TCP FIN would (see ssl_wants_eof_dispatch),
* then honor half-open exactly like the plain-TCP eof branch in
* loop.c. */
if (ssl_wants_eof_dispatch(s)) {
s = ssl_deliver_eof(s);
if (!s || ssl_gone(s)) return NULL;
if (s->flags.allow_half_open) {
/* close_notify only ended the peer's write side; ours may
* still flush queued bytes, and the app's own end() completes
* the shutdown (ssl_handle_shutdown sees RECEIVED_SHUTDOWN and
* finishes immediately). */
return s;
}
}
ssl_close(s, 0, NULL);
return NULL;
}
Expand Down Expand Up @@ -2534,6 +2596,11 @@ void *us_internal_ssl_sni_userdata(struct us_socket_t *s) {
return us_socket_server_name_userdata(s);
}

const char *us_internal_ssl_sni_servername(struct us_socket_t *s) {
if (!s->ssl || !s_ssl(s)) return NULL;
return SSL_get_servername(s_ssl(s), TLSEXT_NAMETYPE_host_name);
}

void us_internal_listen_socket_ssl_free(struct us_listen_socket_t *ls) {
/* Accepted sockets carry `ls` in per-SSL ex_data so sni_cb can reach the
* listener's SNI tree. Those sockets may outlive the listener (server.close()
Expand Down
118 changes: 113 additions & 5 deletions packages/bun-usockets/src/eventing/libuv.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,116 @@

#ifdef LIBUS_USE_LIBUV

/* The shared dispatch follows socket adoption (a tunneled/upgraded socket
* moves; the old allocation stays readable with flags.adopted set and prev
* pointing at the live one) and skips closed sockets. The paused-probe below
* must honor the same contract - dereferencing the raw poll cast crashed the
* CONNECT-tunnel tests on the aarch64 agent. */
/* Windows does not reliably latch a received RST in SO_ERROR (POSIX does);
* the reset surfaces on the next I/O. A zero-byte send observes it without
* touching the stream: 0 on a healthy socket, SOCKET_ERROR with a fatal
* code once the connection died hard. */
int us_internal_libuv_peer_reset_probe(LIBUS_SOCKET_DESCRIPTOR fd) {
/* Raw winsock send: this file only builds on the libuv (Windows) path and
* bsd_send's Windows signature has no flags parameter. */
if (send(fd, "", 0, 0) != SOCKET_ERROR) {
return 0;
}
return WSAGetLastError() != WSAEWOULDBLOCK;
}

static struct us_socket_t *us_internal_poll_cb_adopted_socket(struct us_poll_t *wp) {
struct us_socket_t *s = (struct us_socket_t *)wp;
if (s->flags.adopted && s->prev) {
s = s->prev;
}
return s;
}

static int us_internal_poll_cb_socket_is_probeable(struct us_poll_t *wp) {
struct us_socket_t *s = us_internal_poll_cb_adopted_socket(wp);
return !s->flags.is_closed && s->flags.is_paused;
}

/* uv_poll_t->data always (except for most times after calling us_poll_stop)
* points to the us_poll_t */
static void poll_cb(uv_poll_t *p, int status, int events) {
us_internal_dispatch_ready_poll((struct us_poll_t *)p->data, status < 0 && status != UV_EOF, status == UV_EOF,
events);
/* UV_DISCONNECT (Windows AFD): the peer closed its write side. A FIN
* arriving after this side already half-closed and stopped reading never
* fires another readable poll, and the socket (and server.close()) waits
* forever - so DISCONNECT is armed unconditionally in us_poll_start/change
* and surfaced as a readable dispatch: the read loop's recv() discovers
* the true end of stream (0) after consuming whatever is still queued.
* It is mapped to the eof hint ONLY for sockets whose write side we
* already shut down (see below): unlike kqueue's EV_EOF, which the kernel
* sets only alongside the final data, AFD can signal DISCONNECT while data
* is still in flight, and an unconditional eof mapping closed connections
* at a mid-stream EAGAIN (truncated bodies across the fetch/backpressure
* suites). One-shot: AFD keeps reporting DISCONNECT once signaled, so
* re-arm without it - us_poll_start/us_poll_change add it back on the next
* poll change.
* https://github.com/libuv/libuv/blob/v1.x/docs/src/poll.rst (UV_DISCONNECT
* is Windows-only and best-effort; readable polling stays the primary
* signal). */
int eof = status == UV_EOF;
int error = status < 0 && status != UV_EOF;
if (events & UV_DISCONNECT) {
struct us_poll_t *wp = (struct us_poll_t *)p->data;
uv_poll_start(p, us_poll_events(wp), poll_cb);
int kind = us_internal_poll_type(wp) & POLL_TYPE_KIND_MASK;
/* For a socket whose write side we already shut down, AFD delivers no
* readable event for the peer's FIN at all - the exact half-closed state
* that hung server.close() - and with our writes closed there is no
* data-bearing flow left that an early EOF could truncate. Only there is
* DISCONNECT mapped to the eof hint (like kqueue's EV_EOF); every other
* socket keeps recv()-owned EOF discovery so mid-stream transfers are
* never cut at an EAGAIN. */
if (kind == POLL_TYPE_SOCKET_SHUT_DOWN) {
eof = 1;
events |= UV_READABLE;
} else if (kind == POLL_TYPE_SOCKET && us_internal_poll_cb_socket_is_probeable(wp)) {
/* A paused socket polls without READABLE, so the read loop cannot
* discover terminal states for it - and the pause contract forbids
* consuming deferred bytes. MSG_PEEK discriminates without consuming:
* an error is an abortive reset (our libuv patch reports AFD_POLL_ABORT
* as DISCONNECT so it reaches write-only polls at all) and must close
* now like epoll's unmaskable EPOLLERR; 0 is a graceful FIN with no
* data, deferred by the shared dispatch's existing paused-EOF contract
* until resume; pending data keeps the pause honored untouched. */
char probe;
ssize_t peeked = bsd_recv(us_poll_fd(wp), &probe, 1, MSG_PEEK);
if (peeked == 0) {
eof = 1;
events |= UV_READABLE;
} else if (peeked < 0 && !bsd_would_block()) {
error = 1;
events |= UV_READABLE;
} else if (peeked > 0) {
struct us_socket_t *sock = us_internal_poll_cb_adopted_socket(wp);
if (us_socket_get_error(sock) != 0 || us_internal_libuv_peer_reset_probe(us_poll_fd(wp))) {
/* Data is buffered ahead of whatever ended the connection. If the
* peer ABORTED, the kernel already discarded the stream's tail and
* a paused socket that never resumes would otherwise never learn -
* node's paused sockets error immediately on a reset, buffered
* data included. SO_ERROR separates that from a graceful FIN
* behind data, which stays deferred until resume. */
error = 1;
events |= UV_READABLE;
} else if (!sock->fin_deferred) {
/* Graceful FIN deferred behind data. This one-shot DISCONNECT
* report is now consumed, so a LATER reset (an error-path peer
* ends, flushes, then destroys - FIN, then RST) has no event left
* to ride. Mark the socket; the sweep timer escalates via
* SO_ERROR. */
sock->fin_deferred = 1;
sock->group->loop->data.fin_deferred_count++;
}
}
} else {
events |= UV_READABLE;
}
}
us_internal_dispatch_ready_poll((struct us_poll_t *)p->data, error, eof, events);
}

static void prepare_cb(uv_prepare_t *p) {
Expand Down Expand Up @@ -101,7 +206,10 @@ void us_poll_start(struct us_poll_t *p, struct us_loop_t *loop, int events) {
// a `Async.KeepAlive` associated with them, which is used instead of the
// usockets internals. usockets doesnt have a notion of ref-counted handles.
uv_unref((uv_handle_t *)p->uv_p);
uv_poll_start(p->uv_p, events, poll_cb);
/* Always ask for UV_DISCONNECT: a peer FIN must fire even when the poll is
* writable-only at that moment (a half-closed connection whose reads are
* paused is exactly the state that otherwise hangs; see poll_cb). */
uv_poll_start(p->uv_p, events | UV_DISCONNECT, poll_cb);
}

int us_poll_start_rc(struct us_poll_t *p, struct us_loop_t *loop, int events) {
Expand All @@ -116,7 +224,7 @@ void us_poll_change(struct us_poll_t *p, struct us_loop_t *loop, int events) {
us_internal_poll_type(p) |
((events & LIBUS_SOCKET_READABLE) ? POLL_TYPE_POLLING_IN : 0) |
((events & LIBUS_SOCKET_WRITABLE) ? POLL_TYPE_POLLING_OUT : 0);
uv_poll_start(p->uv_p, events, poll_cb);
uv_poll_start(p->uv_p, events | UV_DISCONNECT, poll_cb);
}
}

Expand Down Expand Up @@ -200,7 +308,7 @@ void us_loop_free(struct us_loop_t *loop) {

us_internal_loop_data_free(loop);

// we need to run the loop one last round to call all close callbacks
// we need to run the loop one last round to call all close callbacks
// we cannot do this if we do not own the loop, default
if (!loop->is_default) {
uv_run(loop->uv_loop, UV_RUN_NOWAIT);
Expand Down
3 changes: 3 additions & 0 deletions packages/bun-usockets/src/internal/eventing/libuv.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
#define LIBUS_SOCKET_READABLE UV_READABLE
#define LIBUS_SOCKET_WRITABLE UV_WRITABLE

/* Defined in eventing/libuv.c; used by the sweep escalation in loop.c. */
int us_internal_libuv_peer_reset_probe(LIBUS_SOCKET_DESCRIPTOR fd);

struct us_loop_t {
alignas(LIBUS_EXT_ALIGNMENT) struct us_internal_loop_data_t data;

Expand Down
14 changes: 14 additions & 0 deletions packages/bun-usockets/src/internal/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ int us_internal_ssl_write(us_socket_r s, const char *data, int length);
void *us_internal_ssl_get_native_handle(us_socket_r s);
struct us_bun_verify_error_t us_internal_ssl_verify_error(us_socket_r s);
void *us_internal_ssl_sni_userdata(us_socket_r s);
const char *us_internal_ssl_sni_servername(us_socket_r s);
void us_internal_ssl_handshake_abort(us_socket_r s);
/* SSL_CTX_free(ls->ssl_ctx) + sni_free(ls->sni). Called from us_listen_socket_close. */
void us_internal_listen_socket_ssl_free(struct us_listen_socket_t *ls);
Expand Down Expand Up @@ -286,6 +287,10 @@ struct us_socket_t {
/* Same as ssl_shutdown_after_spill but for us_internal_ssl_close: the
* close re-runs from the writable event once the spill drains. */
unsigned char ssl_close_after_spill : 1;
/* The plaintext EOF (peer close_notify or the raw TCP FIN behind it) was
* already dispatched to the user layer; both EOF paths can fire for one
* connection, and the end handler must run once. */
unsigned char ssl_end_delivered : 1;
/* Set while SSL_do_handshake/SSL_read is on the stack: JS run from inside
* those calls (ALPN/SNI/keylog callbacks) may destroy the socket, and the
* SSL must not be freed under BoringSSL's feet - the detach is deferred to
Expand All @@ -296,6 +301,15 @@ struct us_socket_t {
* inside a handshake callback must still RST, not FIN, when it is finally
* performed). */
unsigned char ssl_pending_close_code;
/* Consecutive send() failures with an errno that is neither
* would-block/transient nor a known peer-gone error (see
* us_socket_write_check_error). Reset by any send that makes progress.
* Lives in the pad-to-pointer gap before `group`, so it costs nothing. */
/* 7 bits fit the 32-cap retry counter; the spare bit marks a paused
* socket whose peer FIN was deferred behind buffered data (libuv path -
* the sweep escalates via SO_ERROR when the peer later resets). */
unsigned char unclassified_send_failures : 7;
unsigned char fin_deferred : 1;

struct us_socket_group_t *group;
/* NULL for plain TCP. Direct BoringSSL `SSL*`; set by us_internal_ssl_attach
Expand Down
7 changes: 7 additions & 0 deletions packages/bun-usockets/src/internal/loop_data.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ struct us_internal_loop_data_t {
long long sweep_next_tick_ns;
#endif
int sweep_timer_count;
#ifdef LIBUS_USE_LIBUV
/* Sockets whose peer FIN was deferred behind buffered data while paused
* (poll_cb's MSG_PEEK probe): the sweep escalates them via SO_ERROR when
* the peer later resets, since the one-shot DISCONNECT report was already
* consumed by the FIN. Zero cost while no socket is in that state. */
int fin_deferred_count;
#endif
struct us_internal_async *wakeup_async;
struct us_socket_group_t *head;
/* QUIC engines on this loop. us_quic_loop_process walks the list from
Expand Down
5 changes: 5 additions & 0 deletions packages/bun-usockets/src/libusockets.h
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,11 @@ unsigned char us_connecting_socket_kind(struct us_connecting_socket_t *c) nonnul

struct us_bun_verify_error_t us_socket_verify_error(struct us_socket_t *s);

/* SNI hostname the peer sent in its ClientHello (server-side TLS sockets), or
* NULL when the socket is not TLS or no SNI extension was sent. The returned
* pointer is owned by the SSL object and only valid while the socket lives. */
const char *us_socket_sni_servername(struct us_socket_t *s);

/* ── SSL_CTX construction ─────────────────────────────────────────────────
* The expensive bit (cert/key/CA parse, cipher list, DH params) is decoupled
* from sockets entirely. Build once per SecureContext / config, share across
Expand Down
Loading
Loading