diff --git a/lib/core/database/database_error_mapper.dart b/lib/core/database/database_error_mapper.dart index f398253a..6913c674 100644 --- a/lib/core/database/database_error_mapper.dart +++ b/lib/core/database/database_error_mapper.dart @@ -11,6 +11,10 @@ import 'package:querya_desktop/core/security/ssh_tunnel_manager.dart'; import 'package:querya_desktop/core/storage/connection_secrets_store.dart'; /// Which engine produced an error; only used to tailor the wording. +/// The hint of the *server requires SSL* error (#1754); the PostgreSQL form +/// looks for it to offer *Enable SSL and retry*. +const postgresEnableSslHint = 'Enable SSL in the connection settings and try again'; + enum DatabaseDriver { postgres, mysql, sqlite, mongodb, redis, extension } /// Maps a driver / OS error to a [QueryaDatabaseException]. @@ -96,6 +100,24 @@ QueryaDatabaseException mapDatabaseError( originalError: error, stackTrace: stackTrace, ); + } else if (lower.contains('pg_hba.conf rejects connection') && + lower.contains('no encryption')) { + // `hostssl` only in pg_hba.conf: reads like a wrong password or a blocked + // IP, but the fix is the SSL switch (#1754). The server's own line stays in + // the message. + final line = RegExp(r'pg_hba\.conf rejects connection[^\n]*', + caseSensitive: false) + .firstMatch(raw) + ?.group(0) + ?.trim(); + mapped = TlsFailureException( + 'The server only accepts encrypted connections' + '${line == null ? '' : ' ($line)'}', + detailedExplanation: raw, + remediationHint: postgresEnableSslHint, + originalError: error, + stackTrace: stackTrace, + ); } else if (_hasAny(lower, const [ 'password authentication failed', 'access denied for user', diff --git a/lib/features/postgresql/postgresql_connection_form.dart b/lib/features/postgresql/postgresql_connection_form.dart index e1a9fab4..395656bb 100644 --- a/lib/features/postgresql/postgresql_connection_form.dart +++ b/lib/features/postgresql/postgresql_connection_form.dart @@ -751,6 +751,19 @@ class _PostgresConnectionFormContentState ), ).small(), ), + if (!_useSSL && + _testResult!.contains(postgresEnableSslHint)) ...[ + QueryaButton.secondary( + label: 'Enable SSL and retry', + size: QueryaControlSize.sm, + onPressed: () { + _dismissResult(); + setState(() => _useSSL = true); + unawaited(_testConnection()); + }, + ), + const Gap(8), + ], QueryaIconButton( icon: const material.Icon(material.Icons.close), tooltip: 'Dismiss', diff --git a/test/core/database/database_error_mapper_test.dart b/test/core/database/database_error_mapper_test.dart index f899ccd7..c50cd5c7 100644 --- a/test/core/database/database_error_mapper_test.dart +++ b/test/core/database/database_error_mapper_test.dart @@ -388,4 +388,39 @@ void main() { ); }); }); + + group('a server that wants SSL (#1754)', () { + const raw = '28000: pg_hba.conf rejects connection for host "10.10.0.129", ' + 'user "app", database "demo", no encryption'; + + test('is told as an SSL problem, with the server line kept', () { + final mapped = + mapDatabaseError(Exception(raw), driver: DatabaseDriver.postgres); + + expect(mapped, isA()); + expect(mapped.message, contains('only accepts encrypted connections')); + expect(mapped.message, contains('pg_hba.conf rejects connection')); + expect(mapped.message, contains('no encryption')); + expect(mapped.remediationHint, postgresEnableSslHint); + expect(describeDatabaseError(Exception(raw)), contains('Enable SSL')); + }); + + test('is found inside the wrapped connect error too', () { + final wrapped = PostgresConnectionException( + 'Failed to connect to PostgreSQL (demo): ${describeDatabaseError(Exception(raw))}', + cause: Exception(raw), + ); + expect(wrapped.message, contains(postgresEnableSslHint)); + }); + + test('a rejection that is not about missing encryption gets no SSL hint', () { + final mapped = mapDatabaseError( + Exception('28000: pg_hba.conf rejects connection for host "10.0.0.1", ' + 'user "app", database "demo", SSL encryption'), + driver: DatabaseDriver.postgres, + ); + expect(mapped, isNot(isA())); + expect(mapped.remediationHint, isNot(postgresEnableSslHint)); + }); + }); }