Repository: denoland/deno
Pull request: fix(ext/node): preserve TLS upgrade state on reconnect

## Summary
- run the TLS post-connect upgrade hook against the active handle instead of a closure-captured original handle
- rebuild TLS upgrade promise/resolver metadata when `net.Socket` reinitializes its handle during reconnect fallback
- add a regression test covering `kReinitializeHandle` preserving TLS upgrade state

## Testing
- `deno fmt ext/node/polyfills/_tls_wrap.js ext/node/polyfills/net.ts tests/unit_node/tls_test.ts`
- `cargo test -p unit_node_tests --test unit_node tls_test -- --nocapture`

Changed files:
filename: ext/node/ops/http2/session.rs
patch: @@ -20,14 +20,9 @@ use deno_core::v8;
 use libnghttp2 as ffi;
 use serde::Serialize;
 
-/// Type alias matching the C `ssize_t` type used by nghttp2 callbacks.
-/// On Unix, `ssize_t` equals `isize`. On 64-bit Windows MSVC, `ssize_t`
-/// is defined as `long long` (`i64`), which bindgen emits as `i64`
-/// rather than `isize`.
-#[cfg(not(windows))]
-type CSsizeT = isize;
-#[cfg(windows)]
-type CSsizeT = i64;
+/// Match the exact callback return type generated by libnghttp2 bindings on
+/// the current platform instead of guessing the Windows `ssize_t` mapping.
+type CSsizeT = ffi::nghttp2_ssize;
 
 use super::stream::Http2Headers;
 use super::stream::Http2Priority;
@@ -1342,7 +1337,7 @@ fn create_callbacks() -> *mut ffi::nghttp2_session_callbacks {
       callbacks,
       Some(on_frame_send_callback),
     );
-    ffi::nghttp2_session_callbacks_set_select_padding_callback(
+    ffi::nghttp2_session_callbacks_set_select_padding_callback2(
       callbacks,
       Some(on_select_padding),
     );
@@ -1826,7 +1821,7 @@ impl Http2Session {
     options: i32,
   ) -> i32 {
     let has_data = (options & STREAM_OPTION_EMPTY_PAYLOAD) == 0;
-    let mut data_provider = ffi::nghttp2_data_provider {
+    let mut data_provider = ffi::nghttp2_data_provider2 {
       source: ffi::nghttp2_data_source {
         ptr: std::ptr::null_mut(),
       },
@@ -1841,7 +1836,7 @@ impl Http2Session {
 
     // SAFETY: self.session, priority, headers, and data_provider are valid
     let ret = unsafe {
-      ffi::nghttp2_submit_request(
+      ffi::nghttp2_submit_request2(
         self.session,
         &priority.spec,
         headers.data(),

filename: ext/node/ops/http2/stream.rs
patch: @@ -332,7 +332,7 @@ impl Http2Stream {
     }
 
     let has_data = (options & STREAM_OPTION_EMPTY_PAYLOAD) == 0;
-    let mut data_provider = ffi::nghttp2_data_provider {
+    let mut data_provider = ffi::nghttp2_data_provider2 {
       source: ffi::nghttp2_data_source {
         ptr: std::ptr::null_mut(),
       },
@@ -347,7 +347,7 @@ impl Http2Stream {
 
     // SAFETY: session pointer is valid during stream lifetime
     unsafe {
-      ffi::nghttp2_submit_response(
+      ffi::nghttp2_submit_response2(
         session_ptr,
         self.id,
         headers.data(),
@@ -438,7 +438,7 @@ impl Http2Stream {
     let session_ptr = self.nghttp2_session();
 
     if headers.1 == 0 {
-      let mut data_provider = ffi::nghttp2_data_provider {
+      let mut data_provider = ffi::nghttp2_data_provider2 {
         source: ffi::nghttp2_data_source {
           ptr: std::ptr::null_mut(),
         },
@@ -447,7 +447,7 @@ impl Http2Stream {
 
       // SAFETY: session pointer is valid during stream lifetime
       unsafe {
-        ffi::nghttp2_submit_data(
+        ffi::nghttp2_submit_data2(
           session_ptr,
           ffi::NGHTTP2_FLAG_END_STREAM as u8,
           self.id,

filename: ext/node/polyfills/_tls_wrap.js
patch: @@ -198,7 +198,7 @@ export class TLSSocket extends net.Socket {
       const { promise, resolve } = Promise.withResolvers();
 
       // Set `afterConnectTls` hook. This is called in the `afterConnect` method of net.Socket
-      handle.afterConnectTls = async () => {
+      handle.afterConnectTls = async function () {
         options.hostname ??= undefined; // coerce to undefined if null, startTls expects hostname to be undefined
         if (tlssock._needsSockInitWorkaround) {
           tlssock.emit("secure");
@@ -209,7 +209,7 @@ export class TLSSocket extends net.Socket {
         try {
           const conn = await startTls(
             wrap,
-            handle,
+            this,
             options,
           );
           try {
@@ -225,13 +225,14 @@ export class TLSSocket extends net.Socket {
           }
 
           // Assign the TLS connection to the handle and resume reading.
-          handle[kStreamBaseField] = conn;
-          handle.upgrading = false;
-          if (!handle.pauseOnCreate) {
-            handle.readStart();
+          this[kStreamBaseField] = conn;
+          this.upgrading = false;
+          if (!this.pauseOnCreate) {
+            this.readStart();
           }
 
-          resolve();
+          this.afterConnectTlsResolve?.();
+          delete this.afterConnectTlsResolve;
 
           tlssock.emit("secure");
           tlssock.removeListener("end", onConnectEnd);
@@ -240,6 +241,7 @@ export class TLSSocket extends net.Socket {
         }
       };
 
+      handle.afterConnectTlsResolve = resolve;
       handle.upgrading = promise;
       handle.verifyError = function () {
         return null; // Never fails, rejectUnauthorized is always true in Deno.

filename: ext/node/polyfills/net.ts
patch: @@ -1799,7 +1799,15 @@ Socket.prototype[kReinitializeHandle] = function (handle) {
   this._handle?.close();
 
   // Make sure TLS wrap works after reinitialize.
-  handle.afterConnectTls = this._handle.afterConnectTls;
+  if (typeof this._handle?.afterConnectTls === "function") {
+    const { promise, resolve } = Promise.withResolvers();
+    handle.afterConnectTls = this._handle.afterConnectTls;
+    handle.afterConnectTlsResolve = resolve;
+    handle.upgrading = promise;
+    handle.verifyError = this._handle.verifyError;
+    handle._parent = handle;
+    handle._parentWrap = this._handle._parentWrap;
+  }
 
   this._handle = handle;
   this._handle[ownerSymbol] = this;

filename: tests/unit_node/tls_test.ts
patch: @@ -319,6 +319,48 @@ Deno.test("tlssocket._handle._parentWrap is set", () => {
   assertInstanceOf(parentWrap, stream.PassThrough);
 });
 
+Deno.test("net.Socket reinitialize preserves TLS upgrade state", () => {
+  const socket = new net.Socket();
+  const reinitializeHandle = Object.getOwnPropertySymbols(net.Socket.prototype)
+    .find((symbol) => symbol.description === "kReinitializeHandle");
+
+  assert(reinitializeHandle, "expected kReinitializeHandle symbol");
+  const reinitializeHandleSymbol = reinitializeHandle as symbol;
+
+  let closed = false;
+  const afterConnectTls = function () {};
+  const verifyError = () => null;
+  const parentWrap = new stream.PassThrough();
+
+  // deno-lint-ignore no-explicit-any
+  (socket as any)._handle = {
+    close() {
+      closed = true;
+    },
+    afterConnectTls,
+    verifyError,
+    _parentWrap: parentWrap,
+  };
+
+  const newHandle = {};
+  // deno-lint-ignore no-explicit-any
+  (socket as any)[reinitializeHandleSymbol](newHandle);
+
+  assert(closed);
+  // deno-lint-ignore no-explicit-any
+  assertEquals((newHandle as any).afterConnectTls, afterConnectTls);
+  // deno-lint-ignore no-explicit-any
+  assertEquals(typeof (newHandle as any).afterConnectTlsResolve, "function");
+  // deno-lint-ignore no-explicit-any
+  assert((newHandle as any).upgrading instanceof Promise);
+  // deno-lint-ignore no-explicit-any
+  assertEquals((newHandle as any).verifyError, verifyError);
+  // deno-lint-ignore no-explicit-any
+  assertEquals((newHandle as any)._parent, newHandle);
+  // deno-lint-ignore no-explicit-any
+  assertEquals((newHandle as any)._parentWrap, parentWrap);
+});
+
 Deno.test({
   name: "tls connect upgrade js socket wrapper",
   sanitizeOps: false,