Skip to content

node:tls JSStreamSocket deadlocks on multi-packet writes (affects npm:mssql/tedious)Β #33907

Closed

Description

@stefanfaistenauer

Summary

When using npm:mssql (tedious) under Deno's npm compatibility layer, any parameterized query or raw SQL exceeding ~4KB hangs indefinitely. The same queries complete in <1s under Node.js. This affects Deno 2.7.14.

Reproduction

// deno run --allow-net --allow-env --allow-sys --allow-read repro.ts
import sql from "npm:mssql";

const config = {
  server: "yourserver.database.windows.net",
  port: 1433,
  database: "yourdb",
  requestTimeout: 30000,
  options: { encrypt: true },
  authentication: {
    type: "azure-active-directory-password",
    options: { clientId: "...", userName: "...", password: "...", tenantId: "..." },
  },
};

const pool = await sql.connect(config);

// βœ“ Works - small param
const r1 = pool.request();
r1.input("param1", "hello");
await r1.query("SELECT @param1 AS val"); // completes in ~300ms

// βœ— Hangs - 10KB param
const r2 = pool.request();
r2.input("param1", "X".repeat(10000));
await r2.query("SELECT LEN(@param1) AS len"); // hangs forever

Node.js (same mssql version): both complete instantly. Even 1.6MB params complete in <1s.

Environment

  • Deno 2.7.14 (stable, aarch64-apple-darwin)
  • npm:mssql@11.x (tedious@19.x)
  • Azure SQL with encrypt: true (required β€” encrypt: false bypasses the bug but Azure rejects it)

Root Cause Analysis

Tedious wraps its TDS framing in a Duplex stream (native-duplexpair) and passes it to tls.connect({ socket }). Deno creates a JSStreamSocket to bridge this JS stream to the Rust TLS implementation.

The deadlock:

  1. Node.js streams call _write(chunk, cb) one chunk at a time β€” next chunk waits for cb (write completion / InvokeQueued).
  2. For UV/TCP streams, write completion fires via enc_write_cb (async kernel callback).
  3. For JS streams (JSStreamSocket/DuplexPair), enc_write_cb never fires. The only path to InvokeQueued is through cycle(), which is triggered by readBuffer() (incoming peer data).
  4. TDS default packet size is 4096 bytes. Queries >4KB span multiple TDS packets. The server only responds after receiving ALL packets (EOM flag).
  5. Deadlock: First TDS packet encrypts and sends. Write-completion callback never fires (no server response yet β†’ no readBuffer() β†’ no cycle()). Stream won't call _write for the second packet. Server waits for remaining packets. Nobody makes progress.

Why v2.7.14 fixes don't apply

PR #33378 fixed pending_cleartext draining inside enc_write_cb β€” but that callback only fires for UV/TCP streams, not JSStreamSocket. The fix is irrelevant to the tedious/duplexpair code path.

Proposed Fix

In ext/node/polyfills/internal_binding/tls_wrap.ts, after drainEncOut completes, trigger cycle() to fire write-completion for JS streams:

const flushEncOut = () => {
  if (flushPending) return;
  flushPending = true;
  Promise.resolve().then(() => {
    flushPending = false;
    let data;
    while ((data = res.drainEncOut()) && data.byteLength > 0 && jsStreamOwner?.stream) {
      jsStreamOwner.stream.write(new Uint8Array(data));
    }
    // FIX: Trigger cycle() to fire write-completion (InvokeQueued)
    // for JS streams where enc_write_cb never fires.
    res.readBuffer(new Uint8Array(0));
  });
};

This triggers cycle() β†’ enc_out_collect() β†’ InvokeQueued, allowing the next _write call to proceed. It also handles the >48KB case because cycle() calls clear_in() which drains pending_cleartext.

Related Issues

Workaround

Spawn a Node.js subprocess to execute mssql queries, communicating via stdin/stdout JSON.

Activity

  1. added a commit that references this issue on May 7, 2026
    06c1659
  2. added a commit that references this issue on Jun 10, 2026
    21acc20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions