Environment
- pgwire 0.40.7 (latest), server API
- Client:
psql built from PostgreSQL 19beta sources
- Any pgwire-based server is affected; nothing specific to my setup.
What happens
libpq in 19beta defaults max_protocol_version to the reserved GREASE
version 3.9999 ("for 19beta only, will be reverted before RC1" —
fe-connect.c). So every connection from a 19beta libpq sends:
- protocol version
3.9999 in the startup packet
- a startup parameter
_pq_.test_protocol_negotiation (empty value)
and requires the server to report that parameter as unsupported via a
NegotiateProtocolVersion message. pgwire-based servers reject the
connection instead:
psql: error: connection to server at "127.0.0.1", port 5433 failed:
server did not report the unsupported "_pq_.test_protocol_negotiation"
parameter in its protocol negotiation message
This indicates a bug in either the server being contacted
or a proxy handling the connection. Please consider
reporting this to the maintainers of that software.
For more information, including instructions on how to
work around this issue for now, visit
https://wiki.postgresql.org/wiki/Grease
Root cause
protocol_negotiation() in src/api/auth/mod.rs only looks at the
version numbers and never inspects startup parameters:
- known version (3.0/3.2): sets the version and returns — no
NegotiateProtocolVersion is sent even when the packet carries
_pq_. options;
- unknown version with the same major (e.g. 3.9999): sends
NegotiateProtocolVersion::new(newest_server_version.into(), vec![])
— with an empty unsupported-options list.
A PostgreSQL server collects every startup parameter whose name starts
with _pq_. and echoes the unrecognized ones in the
NegotiateProtocolVersion message (backend_startup.c,
ProcessStartupPacket):
else if (strncmp(nameptr, "_pq_.", 5) == 0)
{
/*
* Any option beginning with _pq_. is reserved for use as a
* protocol-level option, but at present no such options are
* defined.
*/
unrecognized_protocol_options =
lappend(unrecognized_protocol_options, pstrdup(nameptr));
}
...
if (PG_PROTOCOL_MINOR(proto) > PG_PROTOCOL_MINOR(PG_PROTOCOL_LATEST) ||
unrecognized_protocol_options != NIL)
SendNegotiateProtocolVersion(unrecognized_protocol_options);
libpq validates the reply strictly (fe-protocol3.c,
pqGetNegotiateProtocolVersion3): every reported option must have been
requested, and every requested _pq_. option must be reported — hence
the error above.
Note this is not a "new protocol version" feature: the _pq_. option
reporting has been part of the protocol since 2017
(postgres commit ae65f6066d, "Provide for forward compatibility with
future minor protocol versions"), and it was back-patched to all
supported releases at the time. It is deliberately orthogonal to the
version number — the commit message says the mechanism "makes it
possible for the client to request previously-unsupported features
without bumping the protocol version at all" — so even a 3.0-only
server must implement it. Every real PostgreSQL server since 2017
passes the GREASE test.
Suggested fix
Collect _pq_.-prefixed parameter names in protocol_negotiation()
and include them in the message in both paths (pgwire currently
supports no protocol options, so all of them are "unrecognized"):
let unsupported_options: Vec<String> = startup_message
.parameters
.keys()
.filter(|name| name.starts_with("_pq_."))
.cloned()
.collect();
- unknown version, same major:
NegotiateProtocolVersion::new(newest.into(), unsupported_options)
- known version: keep the version the client asked for, but still send
NegotiateProtocolVersion::new(version.into(), unsupported_options)
when the list is non-empty (a negotiation message that changes
nothing is a protocol violation on the client side)
I have this implemented and verified against 19beta psql (connection
succeeds, \conninfo shows the negotiated 3.2) and 17.10 psql
(regression-clean). Happy to open a PR.
Workaround
Client side: pin the protocol version, e.g.
psql "host=... max_protocol_version=3.2".
Environment
psqlbuilt from PostgreSQL 19beta sourcesWhat happens
libpq in 19beta defaults
max_protocol_versionto the reserved GREASEversion 3.9999 ("for 19beta only, will be reverted before RC1" —
fe-connect.c). So every connection from a 19beta libpq sends:3.9999in the startup packet_pq_.test_protocol_negotiation(empty value)and requires the server to report that parameter as unsupported via a
NegotiateProtocolVersionmessage. pgwire-based servers reject theconnection instead:
Root cause
protocol_negotiation()insrc/api/auth/mod.rsonly looks at theversion numbers and never inspects startup parameters:
NegotiateProtocolVersionis sent even when the packet carries_pq_.options;NegotiateProtocolVersion::new(newest_server_version.into(), vec![])— with an empty unsupported-options list.
A PostgreSQL server collects every startup parameter whose name starts
with
_pq_.and echoes the unrecognized ones in theNegotiateProtocolVersionmessage (backend_startup.c,ProcessStartupPacket):libpq validates the reply strictly (
fe-protocol3.c,pqGetNegotiateProtocolVersion3): every reported option must have beenrequested, and every requested
_pq_.option must be reported — hencethe error above.
Note this is not a "new protocol version" feature: the
_pq_.optionreporting has been part of the protocol since 2017
(postgres commit ae65f6066d, "Provide for forward compatibility with
future minor protocol versions"), and it was back-patched to all
supported releases at the time. It is deliberately orthogonal to the
version number — the commit message says the mechanism "makes it
possible for the client to request previously-unsupported features
without bumping the protocol version at all" — so even a 3.0-only
server must implement it. Every real PostgreSQL server since 2017
passes the GREASE test.
Suggested fix
Collect
_pq_.-prefixed parameter names inprotocol_negotiation()and include them in the message in both paths (pgwire currently
supports no protocol options, so all of them are "unrecognized"):
NegotiateProtocolVersion::new(newest.into(), unsupported_options)NegotiateProtocolVersion::new(version.into(), unsupported_options)when the list is non-empty (a negotiation message that changes
nothing is a protocol violation on the client side)
I have this implemented and verified against 19beta
psql(connectionsucceeds,
\conninfoshows the negotiated 3.2) and 17.10psql(regression-clean). Happy to open a PR.
Workaround
Client side: pin the protocol version, e.g.
psql "host=... max_protocol_version=3.2".