denoland/deno · error · Error

TLS wrap attach (JS stream) failed: ${attachResult}

Error message

TLS wrap attach (JS stream) failed: ${attachResult}

What it means

The JS-stream variant of the TLS attach step in Deno's tls_wrap binding: when the handle is flagged as JS-backed (JSStreamSocket wrapping a Duplex — I/O routed through JS callbacks), wrap() calls res.attachJsStream() instead of res.attach(). A non-zero status throws 'TLS wrap attach (JS stream) failed: <code>'. Encrypted output is then drained via a pull-based pump mirroring Node's TLSWrap EncOut loop.

Source

Thrown at ext/node/polyfills/internal_binding/tls_wrap.ts:100

    // surfaces as an uncaught exception when the TLSSocket is created
    // inside a native libuv callback (e.g. server_connection_cb) because
    // there is no native TryCatch scope on the call stack.
    res._initError = err;
    return res;
  }

  const nativeHandle = handle;
  res._attachNativeHandle = (nativeHandle: any) =>
    attachNativeHandle(res, nativeHandle);
  res._installNativeOnread = (nativeHandle: any) =>
    installNativeOnread(res, nativeHandle);

  if (nativeHandle[kJSStreamHandle]) {
    // JS-backed stream (e.g. JSStreamSocket wrapping a Duplex).
    // Use attachJsStream instead of attach -I/O goes through JS callbacks.
    const attachResult = res.attachJsStream();
    if (attachResult !== 0) {
      throw new Error(`TLS wrap attach (JS stream) failed: ${attachResult}`);
    }

    // Pull-based encrypted output: instead of Rust calling a JS
    // callback (which causes reentrancy issues), Rust buffers encrypted
    // data and JS drains it after each operation that may produce output.
    //
    // The pump mirrors Node's TLSWrap::EncOut + OnStreamAfterWrite loop
    // (src/crypto/crypto_tls.cc): hand encrypted bytes to the underlying
    // stream, wait for that write to complete, then drive cycle() (the
    // ClearIn + EncOut analog) to push any remaining cleartext, write
    // more encrypted bytes if produced, and finally fire InvokeQueued for
    // the cleartext WriteWrap's oncomplete. For JS-backed streams there
    // is no enc_write_cb (libuv write completion) so cycle() is only
    // driven here; without it the cleartext write callback never fires
    // and writes deadlock (issue #33907). Gating cycle() on the
    // underlying Duplex's _write callback also propagates backpressure,
    // matching Node's behavior.
    const jsStreamOwner = nativeHandle[kOwner];

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Ensure the underlying Duplex/transport is open and fully connected BEFORE calling tls.connect({ socket })
  2. Prefer a real net.Socket (native path) when the transport allows it
  3. On failure, re-establish the tunnel and retry the TLS wrap once with a fresh socket

Example fix

// before
const tlsSock = tls.connect({ socket: tunnelDuplex }); // tunnel not yet connected

// after
await once(tunnelDuplex, 'connect'); // ensure transport is up
const tlsSock = tls.connect({ socket: tunnelDuplex });
Defensive patterns

Strategy: try-catch

Validate before calling

if (tunnel.destroyed || tunnel.writableEnded) {
  throw new Error('tunnel transport not ready for TLS attach');
}
const tlsSock = tls.connect({ socket: tunnel });

Try / catch

try {
  tlsSock = tls.connect({ socket: tunnelDuplex });
} catch (e: any) {
  if (/TLS wrap attach \(JS stream\) failed/.test(e?.message ?? '')) {
    await reEstablishTunnel();
    tlsSock = tls.connect({ socket: tunnelDuplex }); // one retry with fresh transport
  } else throw e;
}

Prevention

When it happens

Trigger: tls.connect over a JS-backed socket — e.g. a custom Duplex transport such as a SOCKS or HTTP-CONNECT tunnel implemented in JS — where the underlying stream is closed, not yet connected, or in a bad state at attach time.

Common situations: TLS over custom transports (proxy tunnels, SSH forwards, test doubles that substitute a Duplex for a real socket); race where the tunnel closes before tls.connect attaches.

Understand the failure class

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/e7d0063e555d56c6. Report an issue: GitHub.