Skip to content

Commit df7c891

Browse files
authored
Tear down the connection when session setup fails in Connection.open() (#20)
* Tear down the connection when session setup fails in Connection.open() After the TCP connection is established (createConnection + createClient) and its 'error'/'close' listeners are registered, if openSession() or requestStatementId() rejects (bad credentials, timeout, or a non-200 status), the catch only logged the error and rethrew — leaking the open socket and its listeners. In the pool this propagates through init() with no close(), so each failed connect attempt leaks a socket. Mirror close()'s teardown (removeAllListeners + destroy/end + null the refs + isConnected=false) in the catch before rethrowing, and add a regression test. Signed-off-by: Zihan Dai <99155080+PDGGK@users.noreply.github.com> * Reset session ids and guard teardown in Connection.open() error path Address review feedback on #20: on a failed session setup the open() catch now (1) clears sessionId/statementId so getSessionId() cannot return a stale id after openSession succeeds but requestStatementId fails, and (2) wraps the socket teardown in its own try/catch so a cleanup failure cannot mask the original error that is rethrown below. Strengthen the regression tests: assert the original setup error surfaces rather than a teardown error, and add a case asserting that a failure after openSession clears the session id. Signed-off-by: Zihan Dai <99155080+PDGGK@users.noreply.github.com> --------- Signed-off-by: Zihan Dai <99155080+PDGGK@users.noreply.github.com>
1 parent c13f1e0 commit df7c891

2 files changed

Lines changed: 88 additions & 0 deletions

File tree

‎src/connection/Connection.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,30 @@ export class Connection {
9292
this.isConnected = true;
9393
} catch (error) {
9494
logger.error("Failed to connect:", error);
95+
// Tear down the half-open connection so its socket and event listeners
96+
// don't leak when session setup (openSession/requestStatementId) fails
97+
// after the TCP connection was already established. Mirrors close().
98+
// Guard the teardown itself so a cleanup failure can't mask the
99+
// original error that we rethrow below.
100+
try {
101+
if (this.connection) {
102+
this.connection.removeAllListeners();
103+
if (typeof this.connection.destroy === "function") {
104+
this.connection.destroy();
105+
} else {
106+
this.connection.end();
107+
}
108+
this.connection = null;
109+
}
110+
} catch (cleanupError) {
111+
logger.warn("Error during connection teardown:", cleanupError);
112+
}
113+
this.client = null;
114+
// Mirror close(): clear session/statement ids so a failed setup does
115+
// not leave a stale sessionId reachable via getSessionId().
116+
this.sessionId = null;
117+
this.statementId = null;
118+
this.isConnected = false;
95119
throw error;
96120
}
97121
}

‎tests/unit/Connection.test.ts‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,68 @@ describe("Connection", () => {
129129

130130
await connection.close();
131131
});
132+
133+
test("Should tear down the socket when session setup fails", async () => {
134+
// openSession rejects after the TCP connection was established.
135+
thriftMock.createClient.mockReturnValueOnce({
136+
openSession: jest.fn((_req: unknown, callback: (e: Error | null, r: unknown) => void) =>
137+
callback(new Error("auth failed"), null),
138+
),
139+
requestStatementId: jest.fn((_sid: unknown, callback: (e: Error | null, r: unknown) => void) =>
140+
callback(null, 456),
141+
),
142+
closeSession: jest.fn((_req: unknown, callback: (e: Error | null, r: unknown) => void) =>
143+
callback(null, { status: { code: 200 } }),
144+
),
145+
});
146+
147+
const config: InternalConfig = {
148+
host: "localhost",
149+
port: 6667,
150+
username: "root",
151+
password: "bad",
152+
enableSSL: false,
153+
sqlDialect: "tree",
154+
};
155+
const connection = new Connection(config);
156+
157+
// The original setup error must surface, not be masked by the teardown.
158+
await expect(connection.open()).rejects.toThrow("auth failed");
159+
160+
// The half-open connection must be torn down (mirrors close()); the buggy
161+
// catch only logged + rethrew, leaking the socket and its listeners.
162+
expect(thriftMock.__mockConnection.removeAllListeners).toHaveBeenCalled();
163+
expect(thriftMock.__mockConnection.destroy).toHaveBeenCalled();
164+
});
165+
166+
test("Should clear sessionId when statement setup fails after openSession", async () => {
167+
// openSession succeeds (sets sessionId), then requestStatementId rejects.
168+
thriftMock.createClient.mockReturnValueOnce({
169+
openSession: jest.fn((_req: unknown, callback: (e: Error | null, r: unknown) => void) =>
170+
callback(null, { status: { code: 200 }, sessionId: 123 }),
171+
),
172+
requestStatementId: jest.fn((_sid: unknown, callback: (e: Error | null, r: unknown) => void) =>
173+
callback(new Error("statement setup failed"), null),
174+
),
175+
closeSession: jest.fn((_req: unknown, callback: (e: Error | null, r: unknown) => void) =>
176+
callback(null, { status: { code: 200 } }),
177+
),
178+
});
179+
180+
const config: InternalConfig = {
181+
host: "localhost",
182+
port: 6667,
183+
username: "root",
184+
password: "root",
185+
enableSSL: false,
186+
sqlDialect: "tree",
187+
};
188+
const connection = new Connection(config);
189+
190+
await expect(connection.open()).rejects.toThrow("statement setup failed");
191+
192+
// The failed setup must not leave a stale sessionId reachable (mirrors
193+
// close()); getSessionId() throws once the id is cleared.
194+
expect(() => connection.getSessionId()).toThrow("Session is not open");
195+
});
132196
});

0 commit comments

Comments
 (0)