Skip to content

Commit 4eb6729

Browse files
TimelordUKclaude
andcommitted
fix: make Connection::Close() idempotent to handle failed connections (#379)
When sql.query() was called with an invalid database, the connection would fail to open but the cleanup code would still try to close it. This threw "Connection is not open" as an uncaught exception instead of passing the error to the callback. Fix: Instead of throwing when Close() is called on a connection that was never opened, just call the callback with success (idempotent close). Fixes #379 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent a555f91 commit 4eb6729

File tree

4 files changed

+142
-5
lines changed

4 files changed

+142
-5
lines changed

cpp/src/js/Connection.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,14 @@ Napi::Value Connection::Close(const Napi::CallbackInfo& info) {
215215
const Napi::Env env = info.Env();
216216
Napi::HandleScope scope(env);
217217

218-
// Check if we have a connection to close
218+
// If connection was never opened or already closed, just call callback with success
219+
// This makes Close() idempotent and prevents crashes when closing failed connections
219220
if (!isConnected_) {
220-
Napi::Error::New(env, "Connection is not open").ThrowAsJavaScriptException();
221+
SQL_LOG_DEBUG("Connection::Close - connection not open, calling callback with success");
222+
if (info.Length() > 0 && info[info.Length() - 1].IsFunction()) {
223+
Napi::Function callback = info[info.Length() - 1].As<Napi::Function>();
224+
callback.Call({env.Null(), Napi::Boolean::New(env, true)});
225+
}
221226
return env.Undefined();
222227
}
223228

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"name": "Stephen James"
1010
}
1111
],
12-
"version": "5.1.2",
12+
"version": "5.1.3",
1313
"homepage": "https://github.com/TimelordUK/node-sqlserver-v8",
1414
"bugs": {
1515
"url": "https://github.com/TimelordUK/node-sqlserver-v8/issues"

test-issue-379.js

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
'use strict'
2+
3+
/**
4+
* Test for Issue #379: sql.query() throws uncaught exception instead of
5+
* passing error to callback when database doesn't exist.
6+
*/
7+
8+
const sql = require('./lib/sql')
9+
10+
// Get base connection string and modify to use non-existent database
11+
function getConnectionString() {
12+
try {
13+
const fs = require('fs')
14+
const path = require('path')
15+
const rcPath = path.join(__dirname, '.env-cmdrc')
16+
if (fs.existsSync(rcPath)) {
17+
const config = JSON.parse(fs.readFileSync(rcPath, 'utf8'))
18+
if (config.test) {
19+
const testConfig = config.test
20+
if (testConfig.CONNECTION_KEY && testConfig[testConfig.CONNECTION_KEY]) {
21+
return testConfig[testConfig.CONNECTION_KEY]
22+
}
23+
}
24+
}
25+
} catch (e) {
26+
// ignore
27+
}
28+
return 'Driver={ODBC Driver 18 for SQL Server};Server=127.0.0.1,1433;Database=node;UID=sa;PWD=Password_123#;TrustServerCertificate=yes;'
29+
}
30+
31+
// Modify connection string to use non-existent database
32+
const baseConnectionString = getConnectionString()
33+
const badConnectionString = baseConnectionString.replace(/Database=[^;]+/, 'Database=NonExistentDB_12345')
34+
35+
console.log('='.repeat(70))
36+
console.log('Test for Issue #379: sql.query() error handling')
37+
console.log('='.repeat(70))
38+
console.log('')
39+
console.log('Base connection:', baseConnectionString.substring(0, 60) + '...')
40+
console.log('Bad connection:', badConnectionString.substring(0, 60) + '...')
41+
console.log('')
42+
43+
// Track if callback was called
44+
let callbackCalled = false
45+
46+
// Set up uncaught exception handler
47+
process.on('uncaughtException', (err) => {
48+
console.error('')
49+
console.error('!!! UNCAUGHT EXCEPTION !!!')
50+
console.error('Error:', err.message)
51+
console.error('Stack:', err.stack)
52+
console.error('')
53+
console.error('This is the bug - error should have gone to callback, not thrown!')
54+
process.exit(1)
55+
})
56+
57+
process.on('unhandledRejection', (reason, promise) => {
58+
console.error('')
59+
console.error('!!! UNHANDLED REJECTION !!!')
60+
console.error('Reason:', reason)
61+
console.error('')
62+
console.error('This is a bug - rejection should have been handled!')
63+
process.exit(1)
64+
})
65+
66+
console.log('Test 1: sql.query() with non-existent database')
67+
console.log('-'.repeat(70))
68+
69+
sql.query(badConnectionString, 'SELECT 1 as test', (err, results) => {
70+
callbackCalled = true
71+
72+
if (err) {
73+
console.log('SUCCESS: Error was passed to callback (expected behavior)')
74+
console.log('Error message:', err.message)
75+
console.log('')
76+
runTest2()
77+
} else {
78+
console.log('UNEXPECTED: Query succeeded?', results)
79+
runTest2()
80+
}
81+
})
82+
83+
// Give it a few seconds, then check if callback was called
84+
setTimeout(() => {
85+
if (!callbackCalled) {
86+
console.error('')
87+
console.error('TIMEOUT: Callback was never called!')
88+
console.error('This might indicate the error was swallowed or thrown elsewhere.')
89+
process.exit(1)
90+
}
91+
}, 10000)
92+
93+
function runTest2() {
94+
console.log('Test 2: sql.open() with non-existent database')
95+
console.log('-'.repeat(70))
96+
97+
sql.open(badConnectionString, (err, conn) => {
98+
if (err) {
99+
console.log('SUCCESS: Error was passed to callback (expected behavior)')
100+
console.log('Error message:', err.message)
101+
console.log('')
102+
runTest3()
103+
} else {
104+
console.log('UNEXPECTED: Connection succeeded?')
105+
conn.close(() => runTest3())
106+
}
107+
})
108+
}
109+
110+
function runTest3() {
111+
console.log('Test 3: sql.promises.query() with non-existent database')
112+
console.log('-'.repeat(70))
113+
114+
sql.promises.query(badConnectionString, 'SELECT 1 as test')
115+
.then(results => {
116+
console.log('UNEXPECTED: Query succeeded?', results)
117+
finishTests()
118+
})
119+
.catch(err => {
120+
console.log('SUCCESS: Promise rejected (expected behavior)')
121+
console.log('Error message:', err.message)
122+
console.log('')
123+
finishTests()
124+
})
125+
}
126+
127+
function finishTests() {
128+
console.log('='.repeat(70))
129+
console.log('All tests completed without uncaught exceptions!')
130+
console.log('='.repeat(70))
131+
process.exit(0)
132+
}

0 commit comments

Comments
 (0)