Repository navigation
Description
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 foreverNode.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: falsebypasses 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:
- Node.js streams call
_write(chunk, cb)one chunk at a time β next chunk waits forcb(write completion /InvokeQueued). - For UV/TCP streams, write completion fires via
enc_write_cb(async kernel callback). - For JS streams (JSStreamSocket/DuplexPair),
enc_write_cbnever fires. The only path toInvokeQueuedis throughcycle(), which is triggered byreadBuffer()(incoming peer data). - TDS default packet size is 4096 bytes. Queries >4KB span multiple TDS packets. The server only responds after receiving ALL packets (EOM flag).
- Deadlock: First TDS packet encrypts and sends. Write-completion callback never fires (no server response yet β no
readBuffer()β nocycle()). Stream won't call_writefor 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
- "socket hang up" when using prisma and adapter-mssqlΒ #32271 β prisma adapter-mssql "socket hang up" with large result sets (same root cause, still open)
- Timeout when trying to connect to a database using the mssql libraryΒ #27833 β mssql connection timeout (likely same root cause)
- fix(ext/node): drain pending TLS cleartext in enc_write_cbΒ #33378 β fixed pending_cleartext draining for UV streams only
Workaround
Spawn a Node.js subprocess to execute mssql queries, communicating via stdin/stdout JSON.
Activity
fix(ext/node): drive TLSWrap cycle on JSStream writes to fix deadlock (β¦
fix(ext/node): drive TLSWrap cycle on JSStream writes to fix deadlock (β¦