fix(frontend): keep connection parameter names case-sensitive

libpq does not fold them: `?SslMode=disable` is rejected as an invalid URI
query parameter rather than read as `sslmode`, which a local server confirms.
Folding made Windmill accept and honour a string Postgres itself refuses;
naming the parameter instead tells the user why it cannot be stored.

The last-value-wins rule for a repeated parameter is unchanged, and matches
what the same server does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem Lemouel
2026-08-19 10:20:44 +02:00
parent fcbcf221f5
commit 312ff5f361
2 changed files with 11 additions and 13 deletions
@@ -138,16 +138,13 @@ describe('unsupportedConnectionParam', () => {
expect(parsePostgresConnectionString(disguised)?.sslmode).toBeUndefined()
})
// libpq matches parameter names case-insensitively. Folding in one reader and not the other
// is what lets a name through the allowlist and past the parser, so the string is saved as
// whatever the default happens to be rather than what it asked for.
it('reads a parameter whatever its case', () => {
// libpq rejects `?SslMode=` as an invalid URI query parameter rather than folding it, so a
// string carrying one does not connect anywhere. Naming it is the honest answer; honouring
// it would save a resource from a URI Postgres itself refuses.
it('refuses a parameter whose name is not the one libpq accepts', () => {
const shouted = 'postgres://u:p@h/db?SslMode=verify-full'
expect(unsupportedConnectionParam(shouted)).toBeUndefined()
expect(parsePostgresConnectionString(shouted)?.sslmode).toBe('verify-full')
expect(unsupportedConnectionParam('postgres://u:p@h/db?Connect_Timeout=1')).toBe(
'connect_timeout'
)
expect(unsupportedConnectionParam(shouted)).toBe('SslMode')
expect(parsePostgresConnectionString(shouted)?.sslmode).toBeUndefined()
})
// libpq takes the last of a repeated parameter. Taking the first reads a weaker mode than
@@ -25,16 +25,17 @@ const CONNECTION_STRING =
/postgres(?:ql)?:\/\/(?<user>[^:@]+)(?::(?<password>[^@]+))?@(?<host>\[[^\]]+\]|[^:\/?]+)(?::(?<port>\d+))?\/(?<dbname>[^\?]+)?/
/**
* The query parameters, read the way libpq reads them: names are case-insensitive, and a name
* The query parameters, read the way libpq reads them: names are case-sensitive — `SslMode` is
* rejected outright as an invalid URI query parameter, not folded to `sslmode` — and a name
* repeated takes its last value. One reader for both the parser and the allowlist below, or
* they disagree about what a string says a name only one of them folds is refused by neither
* and honoured by neither.
* they disagree about what a string says and a name is refused by neither and honoured by
* neither.
*/
function paramsOf(connectionString: string): Map<string, string> {
const query = connectionString.split('?').slice(1).join('?')
const params = new Map<string, string>()
if (!query) return params
new URLSearchParams(query).forEach((value, name) => params.set(name.toLowerCase(), value))
new URLSearchParams(query).forEach((value, name) => params.set(name, value))
return params
}