fix(ext/node): send TLS close_notify on JS stream-backed socket shutdown#35582
Conversation
bartlomieju
left a comment
There was a problem hiding this comment.
Thanks for the careful writeup and the regression test. I confirmed the diagnosis against the Rust side: the shutdown op sends close_notify + do_enc_out_action (which buffers for WriteJs) and then calls underlying.shutdown(), which is a no-op for JS streams (tls_wrap.rs:842), while req.oncomplete(0) fires synchronously — so _final always completed but the close_notify/EOF never reached the wire. The fix is well-targeted and using .end() (half-close) rather than destroy() correctly matches uv_shutdown semantics so the peer can still respond.
My one substantive concern is the .end()-during-handshake case (inline below). I'd also like to see a test for the symmetric client-side .end() and for the handshake-not-yet-complete path, since that's the scenario most likely to regress from this change. Inline notes follow.
Deno Individual Contributor License AgreementAll contributors have signed the CLA. Thank you! This is an automated message from CLA Assistant |
|
Thanks for the careful review. Deferred close_notify (main concern): fixed. The terminal Tests: added the symmetric client-side Worth flagging: writing those tests showed my first versions didn't actually reproduce the bug, which sharpened the root cause:
That's exactly the tedious/Prisma pool path: a connection is used, goes idle, then closed. The tests now use both-JS-backed peers + (Re the |
|
Took a close look — the fix is correct and well-scoped (verified the root cause:
|
|
Thanks. (on "why 1.2 pinned", it mirrors tedious/MSSQL. TDS 7.x caps at TLS 1.2) What I pushed in the latest commit:
|
|
Fix CI regression surfaced by Those tests call Fix: on shutdown, flush the The original hang is still fixed (verified by reverting on the same commit: |
For JS-backed streams (a
TLSSocketwrapping a non-net.SocketDuplex—the mssql/tedious
DuplexPairpattern, also used by Prisma), calling.end()on the TLS socket did not propagate EOF to the peer.
net.Socket's shutdown invokes theshutdownop, which produces the TLSclose_notifyinto the pull-basedpending_enc_outbuffer. But on the JSstream path,
do_enc_out_action(WriteJs)leaves that data buffered (it nevercalls the JS flush), and
UnderlyingStream::Js::shutdown()is a no-op — the uvpath relies on
uv_shutdown, which doesn't exist here. As a result theclose_notifywas never written and the underlying stream was never ended, sothe peer never observed EOF and hung. In tedious/Prisma against MSSQL this
surfaces as
Connection lost - socket hang up.Wrap
shutdownfor JS-backed streams so that after the native op runs we flushthe buffered encrypted output (including
close_notify) through the existingpump and then
end()the underlying stream once drained, mirroringuv_shutdownon the native path.Adds a regression test: a server-side
TLSSocketover a back-to-backDuplexpair must propagate
close_notify/EOF on.end().Fixes #32271. Even though it is marked as Closed, I have confirmed that it is still valid on the latest code.
I used Cursor (Claude) to help investigate and write this change.