Repository navigation
Provide structured errors for invalid connection parameters #28
Description
parse_dsn reports every connection-string problem as an ArgumentError whose only content is the
message. A caller that wants to know which parameter failed, for example to show its own message
without repeating user input, has to parse that text. That breaks whenever the wording changes. 2.3.0
added connection parameter "<key>" is set to a value Postgres.jl does not support and cannot safely ignore, for example. It can also be misled today, because the invalid value messages insert the
value without escaping it:
julia> Postgres.parse_dsn("host=h port='1\" for connection parameter \"password'")
ERROR: ArgumentError: invalid value "1" for connection parameter "password" for connection parameter "port"; expected an integerI'd like to propose three small changes. Each could be its own PR, and none changes the exception type
parse_dsn throws.
1. Escape the echoed value (2.x patch)
In parse_optional_int and parse_bool_param, write "\"$(escape_string(value))\"" instead of
"\"$value\"". The message above becomes
invalid value "1\" for connection parameter \"password" for connection parameter "port"; ….
The output only changes for a value that contains ", \ or a control character. escape_string
leaves $ alone, so an unexpanded ${PGPORT} still prints as is.
2. An additive, non-throwing check (2.x minor)
"""
Postgres.validate_dsn(dsn) -> Union{Nothing, Postgres.ConnectionParameterError}
Return `nothing` if `parse_dsn(dsn)` would succeed, otherwise the error describing why it would not.
"""
struct ConnectionParameterError <: Exception
key::Union{String, Nothing} # a recognized parameter name, never text from a value
reason::Symbol # :unknown_key, :invalid_value, :unsupported_ignored, :malformed
msg::String # exactly the message parse_dsn throws today
end
Base.showerror(io::IO, e::ConnectionParameterError) = print(io, e.msg)parse_dsnand all of itsArgumentErrors stay exactly as they are. REPL output,catch e::ArgumentError
and@test_throws ArgumentErrorall keep working.showerrorprints today's message unchanged, so a caller that shows the returned error gets the same
textparse_dsnwould have thrown.keycomes only from the parser's key side, after the key has been recognized. The parser already
checks every key before it echoes one, sokeyis always one of the known or ignored keywords. It is
nothingfor an unknown key, an empty key, and an unparseable URI. Thenumeric_overflowcheck
reportskey = "numeric_overflow".- Mapping of the current throw sites: unknown key →
:unknown_key; integer, boolean and
numeric_overflowvalues →:invalid_value; the SECURITY_SENSITIVE_IGNORED check →
:unsupported_ignored; the keyword-parser syntax errors and the URI error →:malformed. - Implementation: one internal function parses and returns either the parsed result or a
ConnectionParameterError, without throwing.validate_dsnreturnsnothingor that error.
parse_dsnthrowsArgumentError(e.msg)when it gets an error. Neither path wraps the other in a
try/catch, so a successful parse never pays for an exception, and there is no exception chain
that could carry input.
If you'd prefer one function to two, a per-call keyword, parse_dsn(dsn; structured = true), that
throws ConnectionParameterError instead of ArgumentError would work too.
Every current message shape, with a minimal input
| input | message | echoes | key / reason |
|---|---|---|---|
"host=h ssl_mode=verify-full" |
unrecognized connection parameter; recognized parameters are … |
— | nothing / :unknown_key |
"host=h channel_binding=require" |
connection parameter "channel_binding" is set to a value Postgres.jl does not support and cannot safely ignore |
key | "channel_binding" / :unsupported_ignored |
"host=h port=abc" |
invalid value "abc" for connection parameter "port"; expected an integer |
key, value | "port" / :invalid_value |
"host=h reconnect=ture" |
invalid value "ture" for connection parameter "reconnect"; expected a boolean (on/off, true/false, yes/no, 1/0) |
key, value | "reconnect" / :invalid_value |
"host=h numeric_overflow=ignore" |
numeric_overflow must be :warn or :error |
— | "numeric_overflow" / :invalid_value |
"=x" |
empty connection parameter name |
— | nothing / :malformed |
"host=h user" |
connection parameter "user" is missing '=' |
key | "user" / :malformed |
"host=abc\\" |
dangling escape in value for connection parameter "host" |
key | "host" / :malformed |
"host='unterminated" |
unterminated quoted value for connection parameter "host" |
key | "host" / :malformed |
"host='h'trailing" |
unexpected text after quoted value for connection parameter "host" |
key | "host" / :malformed |
"postgresql://u:p@h:notaport/db" |
invalid PostgreSQL URI; check syntax and percent-encoding |
— | nothing / :malformed |
Question: value-free text. Only :invalid_value messages contain a value, and only for integer and
boolean parameters, so key + reason already let a caller decide what is safe to show. If you'd like
it built in, showerror could honor IOContext(io, :redact => true) and print
invalid value for connection parameter "port"; expected an integer. I'm equally happy to leave that out.
3. Validate sslmode and gssencmode while parsing (2.x minor)
Today an invalid value passes parse_dsn and is only rejected when connecting:
julia> p = Postgres.parse_dsn("host=h sslmode=verfy-full gssencmode=bogus"); # succeeds
julia> Postgres.Connection(host = "127.0.0.1", gssencmode = "bogus")
ERROR: Postgres.Error: … Message: invalid gssencmode: bogusThe gssencmode check runs before any dial. The sslmode check runs only after the TCP connection is up
(API.jl:801). libpq rejects both while it parses the options. Proposal:
- One list of accepted values (
disable,prefer,require,verify-fullfor sslmode;disable,
prefer,requirefor gssencmode), shared by the parser and the connect path. The lists are constant
tuples, so the parse-time check allocates nothing. The only added cost is one lookup per connection,
next to a network round trip. - Check it in the
ConnectionParamskeyword constructor, which runs afterPGSSLMODE/PGGSSENCMODE
have been merged in. Check it in theConnectionkeyword constructor too, asnumeric_overflowalready is. - Keep today's case-insensitivity, and add no new values. The message matches the other value errors,
with the escaped value:
invalid value "verfy-full" for connection parameter "sslmode"; expected one of disable, prefer, require, verify-full.
validate_dsnwould reportkey = "sslmode"/"gssencmode"andreason = :invalid_value. - Tests: every value the connect path accepts today, in any case and from either the DSN or the
environment, is still accepted byparse_dsn, and each invalid value is refused at parse time.
Inputs that connect today are unaffected, with one exception (case 3). Three cases do change:
- A caller that only calls
parse_dsn, for example to check a config without connecting. It gets an
ArgumentErrorwhere it got aConnectionParamsbefore. This also applies when the bad value comes
fromPGSSLMODEorPGGSSENCMODE. - A caller that catches
Postgres.Errorforinvalid sslmode/invalid gssencmodeat connect time.
It now gets anArgumentError, earlier, fromparse_dsnor from theConnectionconstructor. - An invalid
sslmodeover GSS encryption. When GSS encryption is negotiated,API.connectreturns
atAPI.jl:760before the sslmode check, sogssencmode=require sslmode=boguscurrently connects on
a machine with a Kerberos ticket. The same config already fails on a machine without one, and libpq
rejects it everywhere. The PR would also move the connect-time sslmode check next to the gssencmode
check, so no path skips it.
Because of these cases I'd mention it in the release notes, as a minor-version change.
Possible future 3.0 (not part of this request)
parse_dsn itself could throw ConnectionParameterError. That would be breaking, because ArgumentError
is concrete and cannot be subtyped, so it only makes sense if a 3.0 happens for other reasons.
Context: I maintain PormG.jl, which uses Postgres.jl as a backend. Its handling of these errors matched on
the message text, and the new 2.3.0 message broke it the same day (PingoLee/PormG.jl#1233, fixed in
PingoLee/PormG.jl#1234). That is what prompted this issue.
I'm happy to send all three as separate PRs, starting with the escaping fix, once you're happy with
the shape.
This issue was drafted with Claude Code (Claude Opus 5.5). I reviewed the analysis, and the snippets were run against v2.3.0.
Activity
Bump version to 2.3.1
quinnj commentedon Oct 27, 2026
Thanks for the detailed proposal. The escaping fix is already merged in #32. Please send separate PRs for the remaining two changes:
Postgres.validate_dsnAPI returning structured error information, while preservingparse_dsn's existingArgumentErrorbehavior.sslmodeandgssencmode, before connecting, including environment-derived values and the GSS path. Preserve the currently accepted values and case-insensitivity, and document the earlier errors as a behavior change.AI disclosure: Codex investigated the current code and drafted this response; I approve the suggestions above.