From 315a43149d813bd20e5af4e2b24196b15a7b3215 Mon Sep 17 00:00:00 2001 From: tahodev <85001874+tahodev@users.noreply.github.com> Date: Sat, 26 Sep 2026 02:56:28 +0900 Subject: [PATCH 1/2] fix(server): avoid plaintext retry for Postgres SQL errors --- .../data-source-query/execute-query.test.ts | 30 +++++++++++++++++++ .../data-source-query/postgres-transport.ts | 8 +++++ 2 files changed, 38 insertions(+) diff --git a/packages/server/src/services/data-source-query/execute-query.test.ts b/packages/server/src/services/data-source-query/execute-query.test.ts index 3fe22569..3ea087ec 100644 --- a/packages/server/src/services/data-source-query/execute-query.test.ts +++ b/packages/server/src/services/data-source-query/execute-query.test.ts @@ -587,6 +587,36 @@ describe("data source query execution", () => { }); }); + it("does not retry a PostgreSQL query error over plaintext", async () => { + const sqlError = Object.assign( + new Error("syntax error at or near SELECT"), + { + code: "42601", + } + ); + const { receivedConfigs, runner } = createPostgresRunner([ + { connectError: sqlError }, + { rows: [{ result: 1 }] }, + ]); + + const result = await executePostgresQuery( + { + ...postgresCredentials, + sslMode: "prefer", + type: "postgres", + }, + "SELECT broken", + undefined, + runner + ); + + expect(result.isErr()).toBe(true); + expect(receivedConfigs).toHaveLength(1); + expect(receivedConfigs[0]).toMatchObject({ + ssl: { rejectUnauthorized: true }, + }); + }); + it("preserves the prior transport error when plaintext fallback also fails", async () => { const initialError = new Error("connection reset by peer"); const { receivedConfigs, runner } = createPostgresRunner([ diff --git a/packages/server/src/services/data-source-query/postgres-transport.ts b/packages/server/src/services/data-source-query/postgres-transport.ts index e08381c1..eaf8e2bf 100644 --- a/packages/server/src/services/data-source-query/postgres-transport.ts +++ b/packages/server/src/services/data-source-query/postgres-transport.ts @@ -69,6 +69,14 @@ export function resolvePostgresFailureTransitions( return []; } + // PostgreSQL SQLSTATE codes indicate the server accepted the connection. + // Retrying a rejected query on plaintext cannot fix the SQL error and would + // needlessly send credentials and the query again over an insecure link. + const code = readErrorCode(error); + if (code && /^[0-9A-Z]{5}$/u.test(code)) { + return []; + } + if (!isTlsVerificationError(error)) { return [ { From a5e5c02b016567333e53486562c7341f4bdb2d42 Mon Sep 17 00:00:00 2001 From: tahodev <85001874+tahodev@users.noreply.github.com> Date: Sat, 26 Sep 2026 02:57:03 +0900 Subject: [PATCH 2/2] test(server): retain fallback for Postgres connection exceptions --- .../data-source-query/execute-query.test.ts | 28 +++++++++++++++++++ .../data-source-query/postgres-transport.ts | 9 +++--- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/packages/server/src/services/data-source-query/execute-query.test.ts b/packages/server/src/services/data-source-query/execute-query.test.ts index 3ea087ec..b53e812a 100644 --- a/packages/server/src/services/data-source-query/execute-query.test.ts +++ b/packages/server/src/services/data-source-query/execute-query.test.ts @@ -617,6 +617,34 @@ describe("data source query execution", () => { }); }); + it("keeps plaintext fallback for a PostgreSQL connection exception", async () => { + const { receivedConfigs, runner } = createPostgresRunner([ + { + connectError: Object.assign(new Error("connection failure"), { + code: "08006", + }), + }, + { rows: [{ result: 1 }] }, + ]); + + const rows = unwrapQueryResult( + await executePostgresQuery( + { + ...postgresCredentials, + sslMode: "prefer", + type: "postgres", + }, + "SELECT 1", + undefined, + runner + ) + ); + + expect(rows).toEqual([{ result: 1 }]); + expect(receivedConfigs).toHaveLength(2); + expect(receivedConfigs[1]).toMatchObject({ ssl: false }); + }); + it("preserves the prior transport error when plaintext fallback also fails", async () => { const initialError = new Error("connection reset by peer"); const { receivedConfigs, runner } = createPostgresRunner([ diff --git a/packages/server/src/services/data-source-query/postgres-transport.ts b/packages/server/src/services/data-source-query/postgres-transport.ts index eaf8e2bf..547f1381 100644 --- a/packages/server/src/services/data-source-query/postgres-transport.ts +++ b/packages/server/src/services/data-source-query/postgres-transport.ts @@ -69,11 +69,12 @@ export function resolvePostgresFailureTransitions( return []; } - // PostgreSQL SQLSTATE codes indicate the server accepted the connection. - // Retrying a rejected query on plaintext cannot fix the SQL error and would - // needlessly send credentials and the query again over an insecure link. + // Non-connection SQLSTATE codes mean the server accepted the connection. + // Retrying the rejected query on plaintext cannot fix the SQL error and + // needlessly sends credentials and the query again over an insecure link. + // Class 08 is a connection exception, so keep its transport fallback. const code = readErrorCode(error); - if (code && /^[0-9A-Z]{5}$/u.test(code)) { + if (code && /^[0-9A-Z]{5}$/u.test(code) && !code.startsWith("08")) { return []; }