libpq: Bump protocol version to version 3.2 at least until the first/second beta

Started by Jelte Fennema-Nio3 months ago17 messages
#1Jelte Fennema-Nio
postgres@jeltef.nl
2 attachment(s)

The main reason that libpq does not request protocol version 3.2 by
default is because other proxy/server implementations don't implement
the negotiation. This is a bit of a chicken and egg problem: We don't
bump the default version that libpq asks, but proxies will only
implement version negotation when their users run into issues. So I'm
proposing that we bump the default protocol version that libpq requests
on the master branch, so that users running the latest master against a
proxy that does not support the version negototian will be notified.
They can then push the author of the proxy to implement the
NegototiateProtocolVersion message.

Depending on how this works in practice we'll likely still want to
revert this change before we actually release PG19. If we do that before
19beta1 we still have roughly half a year where people will test the
ecosystem. I'd even suggest that we only revert before 19beta2, so that
people testing beta1 will also be testing the ecosystem for version
negotiation issues. In any case, the sooner we commit this the more
testing we get,

Note that users still have a way out to connect to the server by
manually setting max_protocol_version=3.0 in the connection string.

Attachments:

v1-0001-Add-test-for-libpq-its-default-protocol-version.patchtext/x-patch; charset=utf-8; name=v1-0001-Add-test-for-libpq-its-default-protocol-version.patchDownload
From 0486ee9f14dbfc4fb19d7716f2ce75d5fcbed2e5 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Thu, 23 Oct 2025 15:16:05 +0200
Subject: [PATCH v1 1/2] Add test for libpq its default protocol version

We did not test libpq its default protocol version. Defaults are
important so we should test them.
---
 .../modules/libpq_pipeline/libpq_pipeline.c   | 33 ++++++++++++++++---
 1 file changed, 28 insertions(+), 5 deletions(-)

diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c
index b3af70fa09b..3af79e03d24 100644
--- a/src/test/modules/libpq_pipeline/libpq_pipeline.c
+++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c
@@ -1328,7 +1328,7 @@ test_protocol_version(PGconn *conn)
 	int			nopts;
 	PQconninfoOption *opts = PQconninfo(conn);
 	int			protocol_version;
-	int			max_protocol_version_index;
+	int			max_protocol_version_index = -1;
 	int			i;
 
 	/*
@@ -1351,14 +1351,37 @@ test_protocol_version(PGconn *conn)
 		{
 			keywords[i] = opt->keyword;
 			vals[i] = opt->val;
+			if (strcmp(opt->keyword, "max_protocol_version") == 0)
+			{
+				max_protocol_version_index = i;
+			}
+
 			i++;
 		}
 	}
 
-	max_protocol_version_index = i;
-	keywords[i] = "max_protocol_version";	/* value is filled in below */
-	i++;
-	keywords[i] = vals[i] = NULL;
+	if (max_protocol_version_index == -1)
+	{
+		max_protocol_version_index = i;
+		keywords[i] = "max_protocol_version";	/* value is filled in below */
+		i++;
+	}
+
+	/*
+	 * Test default protocol_version
+	 */
+	vals[max_protocol_version_index] = "";
+	conn = PQconnectdbParams(keywords, vals, false);
+
+	if (PQstatus(conn) != CONNECTION_OK)
+		pg_fatal("Connection to database failed: %s",
+				 PQerrorMessage(conn));
+
+	protocol_version = PQfullProtocolVersion(conn);
+	if (protocol_version != 30000)
+		pg_fatal("expected 30000, got %d", protocol_version);
+
+	PQfinish(conn);
 
 	/*
 	 * Test max_protocol_version=3.0
-- 
2.51.1

v1-0002-libpq-Bump-default-protocol-version-to-3.2.patchtext/x-patch; charset=utf-8; name=v1-0002-libpq-Bump-default-protocol-version-to-3.2.patchDownload
From 57a89e1780bd721c721ee64a04ade165fd83a836 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Thu, 23 Oct 2025 15:08:52 +0200
Subject: [PATCH v1 2/2] libpq: Bump default protocol version to 3.2

This bumps the default protocol version that libpq uses to connect to
servers. This will be reverted before we release 19beta2. The intent is
to stress test the ecosystem for servers that don't support protocol
version negotation, so that those servers can implement the negotation.

It's still possible for users to connect to servers that don't support
protocol negotation by using max_protocol_version=3.0 in their
connection string. Only the default connection behaviour is impacted.
---
 doc/src/sgml/libpq.sgml                          | 16 +++++++---------
 src/interfaces/libpq/fe-connect.c                | 12 ++++--------
 src/test/modules/libpq_pipeline/libpq_pipeline.c |  4 ++--
 3 files changed, 13 insertions(+), 19 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 5bf59a19855..686a473ccba 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2213,15 +2213,13 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       <listitem>
        <para>
         Specifies the protocol version to request from the server.
-        The default is to use version <literal>3.0</literal> of the
-        <productname>PostgreSQL</productname> protocol, unless the connection
-        string specifies a feature that relies on a higher protocol version,
-        in which case the latest version supported by libpq is used. If the
-        server does not support the protocol version requested by the client,
-        the connection is automatically downgraded to a lower minor protocol
-        version that the server supports. After the connection attempt has
-        completed you can use <xref linkend="libpq-PQprotocolVersion"/> to
-        find out which exact protocol version was negotiated.
+        The default is to use the latest version supported by libpq, which is
+        currently <literal>3.2</literal>. If the server does not support the
+        protocol version requested by the client, the connection is
+        automatically downgraded to a lower minor protocol version that the
+        server supports. After the connection attempt has completed you can
+        use <xref linkend="libpq-PQprotocolVersion"/> to find out which exact
+        protocol version was negotiated.
        </para>
 
        <para>
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index a3d12931fff..0bd5ef22298 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -2131,15 +2131,11 @@ pqConnectOptions2(PGconn *conn)
 	else
 	{
 		/*
-		 * To not break connecting to older servers/poolers that do not yet
-		 * support NegotiateProtocolVersion, default to the 3.0 protocol at
-		 * least for a while longer. Except when min_protocol_version is set
-		 * to something larger, then we might as well default to the latest.
+		 * Default to the latest protocol version. The server will
+		 * automatically downgrade via NegotiateProtocolVersion if it doesn't
+		 * support the requested version.
 		 */
-		if (conn->min_pversion > PG_PROTOCOL(3, 0))
-			conn->max_pversion = PG_PROTOCOL_LATEST;
-		else
-			conn->max_pversion = PG_PROTOCOL(3, 0);
+		conn->max_pversion = PG_PROTOCOL_LATEST;
 	}
 
 	if (conn->min_pversion > conn->max_pversion)
diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c
index 3af79e03d24..374917acacf 100644
--- a/src/test/modules/libpq_pipeline/libpq_pipeline.c
+++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c
@@ -1378,8 +1378,8 @@ test_protocol_version(PGconn *conn)
 				 PQerrorMessage(conn));
 
 	protocol_version = PQfullProtocolVersion(conn);
-	if (protocol_version != 30000)
-		pg_fatal("expected 30000, got %d", protocol_version);
+	if (protocol_version != 30002)
+		pg_fatal("expected 30002, got %d", protocol_version);
 
 	PQfinish(conn);
 
-- 
2.51.1

#2Jelte Fennema-Nio
postgres@jeltef.nl
In reply to: Jelte Fennema-Nio (#1)
2 attachment(s)
Re: libpq: Bump protocol version to version 3.2 at least until the first/second beta

On Thu Oct 23, 2025 at 3:56 PM CEST, Jelte Fennema-Nio wrote:

So I'm
proposing that we bump the default protocol version that libpq requests
on the master branch, so that users running the latest master against a
proxy that does not support the version negototian will be notified.

Rebased to resolve conflict

Attachments:

v2-0001-Add-test-for-libpq-its-default-protocol-version.patchtext/x-patch; charset=utf-8; name=v2-0001-Add-test-for-libpq-its-default-protocol-version.patchDownload
From 051e152bbb9ef6ed467aacd3c0a82a69fc8f8765 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Thu, 23 Oct 2025 15:16:05 +0200
Subject: [PATCH v2 1/2] Add test for libpq its default protocol version

We did not test libpq its default protocol version. Defaults are
important so we should test them.
---
 .../modules/libpq_pipeline/libpq_pipeline.c   | 33 ++++++++++++++++---
 1 file changed, 28 insertions(+), 5 deletions(-)

diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c
index b3af70fa09b..3af79e03d24 100644
--- a/src/test/modules/libpq_pipeline/libpq_pipeline.c
+++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c
@@ -1328,7 +1328,7 @@ test_protocol_version(PGconn *conn)
 	int			nopts;
 	PQconninfoOption *opts = PQconninfo(conn);
 	int			protocol_version;
-	int			max_protocol_version_index;
+	int			max_protocol_version_index = -1;
 	int			i;
 
 	/*
@@ -1351,14 +1351,37 @@ test_protocol_version(PGconn *conn)
 		{
 			keywords[i] = opt->keyword;
 			vals[i] = opt->val;
+			if (strcmp(opt->keyword, "max_protocol_version") == 0)
+			{
+				max_protocol_version_index = i;
+			}
+
 			i++;
 		}
 	}
 
-	max_protocol_version_index = i;
-	keywords[i] = "max_protocol_version";	/* value is filled in below */
-	i++;
-	keywords[i] = vals[i] = NULL;
+	if (max_protocol_version_index == -1)
+	{
+		max_protocol_version_index = i;
+		keywords[i] = "max_protocol_version";	/* value is filled in below */
+		i++;
+	}
+
+	/*
+	 * Test default protocol_version
+	 */
+	vals[max_protocol_version_index] = "";
+	conn = PQconnectdbParams(keywords, vals, false);
+
+	if (PQstatus(conn) != CONNECTION_OK)
+		pg_fatal("Connection to database failed: %s",
+				 PQerrorMessage(conn));
+
+	protocol_version = PQfullProtocolVersion(conn);
+	if (protocol_version != 30000)
+		pg_fatal("expected 30000, got %d", protocol_version);
+
+	PQfinish(conn);
 
 	/*
 	 * Test max_protocol_version=3.0

base-commit: 8a27d418f8fc08b62f371c1b167efbfbf0a2a24e
-- 
2.51.1

v2-0002-libpq-Bump-default-protocol-version-to-3.2.patchtext/x-patch; charset=utf-8; name=v2-0002-libpq-Bump-default-protocol-version-to-3.2.patchDownload
From 19cdc47301bd3e81a65b029bee9b45e71bcf5b82 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Thu, 23 Oct 2025 15:08:52 +0200
Subject: [PATCH v2 2/2] libpq: Bump default protocol version to 3.2

This bumps the default protocol version that libpq uses to connect to
servers. This will be reverted before we release 19beta2. The intent is
to stress test the ecosystem for servers that don't support protocol
version negotation, so that those servers can implement the negotation.

It's still possible for users to connect to servers that don't support
protocol negotation by using max_protocol_version=3.0 in their
connection string. Only the default connection behaviour is impacted.
---
 doc/src/sgml/libpq.sgml                          | 16 +++++++---------
 src/interfaces/libpq/fe-connect.c                | 12 ++++--------
 src/test/modules/libpq_pipeline/libpq_pipeline.c |  4 ++--
 3 files changed, 13 insertions(+), 19 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index e76da383714..cc261f8eceb 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2213,15 +2213,13 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       <listitem>
        <para>
         Specifies the protocol version to request from the server.
-        The default is to use version <literal>3.0</literal> of the
-        <productname>PostgreSQL</productname> protocol, unless the connection
-        string specifies a feature that relies on a higher protocol version,
-        in which case the latest version supported by libpq is used. If the
-        server does not support the protocol version requested by the client,
-        the connection is automatically downgraded to a lower minor protocol
-        version that the server supports. After the connection attempt has
-        completed you can use <xref linkend="libpq-PQfullProtocolVersion"/> to
-        find out which exact protocol version was negotiated.
+        The default is to use the latest version supported by libpq, which is
+        currently <literal>3.2</literal>. If the server does not support the
+        protocol version requested by the client, the connection is
+        automatically downgraded to a lower minor protocol version that the
+        server supports. After the connection attempt has completed you can
+        use <xref linkend="libpq-PQfullProtocolVersion"/> to find out which
+        exact protocol version was negotiated.
        </para>
 
        <para>
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index a3d12931fff..0bd5ef22298 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -2131,15 +2131,11 @@ pqConnectOptions2(PGconn *conn)
 	else
 	{
 		/*
-		 * To not break connecting to older servers/poolers that do not yet
-		 * support NegotiateProtocolVersion, default to the 3.0 protocol at
-		 * least for a while longer. Except when min_protocol_version is set
-		 * to something larger, then we might as well default to the latest.
+		 * Default to the latest protocol version. The server will
+		 * automatically downgrade via NegotiateProtocolVersion if it doesn't
+		 * support the requested version.
 		 */
-		if (conn->min_pversion > PG_PROTOCOL(3, 0))
-			conn->max_pversion = PG_PROTOCOL_LATEST;
-		else
-			conn->max_pversion = PG_PROTOCOL(3, 0);
+		conn->max_pversion = PG_PROTOCOL_LATEST;
 	}
 
 	if (conn->min_pversion > conn->max_pversion)
diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c
index 3af79e03d24..374917acacf 100644
--- a/src/test/modules/libpq_pipeline/libpq_pipeline.c
+++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c
@@ -1378,8 +1378,8 @@ test_protocol_version(PGconn *conn)
 				 PQerrorMessage(conn));
 
 	protocol_version = PQfullProtocolVersion(conn);
-	if (protocol_version != 30000)
-		pg_fatal("expected 30000, got %d", protocol_version);
+	if (protocol_version != 30002)
+		pg_fatal("expected 30002, got %d", protocol_version);
 
 	PQfinish(conn);
 
-- 
2.51.1

#3Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Jelte Fennema-Nio (#1)
Re: libpq: Bump protocol version to version 3.2 at least until the first/second beta

On Thu, Oct 23, 2025 at 6:56 AM Jelte Fennema-Nio <me@jeltef.nl> wrote:

Depending on how this works in practice we'll likely still want to
revert this change before we actually release PG19. If we do that before
19beta1 we still have roughly half a year where people will test the
ecosystem.

I think the whole plan probably belongs in the user documentation.
Even if no one were to read it, I still wouldn't want the declaration
that "we default to the latest" to be mixed into the growing search
engine slop pile.

Is there an even stronger way for us to grease this? For example,
could we agree that no one will ever implement 0003.7FFF and push that
during the beta, failing if anyone gives us an unsupported version?

--Jacob

#4Jelte Fennema-Nio
postgres@jeltef.nl
In reply to: Jacob Champion (#3)
Re: libpq: Bump protocol version to version 3.2 at least until the first/second beta

On Fri, 31 Oct 2025 at 17:24, Jacob Champion
<jacob.champion@enterprisedb.com> wrote:

I still wouldn't want the declaration
that "we default to the latest" to be mixed into the growing search
engine slop pile.

Fair enough (although the intent is to get to that state at some point).

Is there an even stronger way for us to grease this? For example,
could we agree that no one will ever implement 0003.7FFF and push that
during the beta, failing if anyone gives us an unsupported version?

I quite like that idea! Although maybe not 7FFF, but 270F so that
PQfullProtocolVersion returns 39999. And I think it'd be good to also
add a protocol option, like _pq_.test_protocol_breakage and fail the
connection attempt client side if that does not get returned back as
unsupported. I'll try to update this patch to do that in the coming days.

#5Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Jelte Fennema-Nio (#4)
Re: libpq: Bump protocol version to version 3.2 at least until the first/second beta

On Fri, Oct 31, 2025 at 2:56 PM Jelte Fennema-Nio <me@jeltef.nl> wrote:

I quite like that idea! Although maybe not 7FFF, but 270F so that
PQfullProtocolVersion returns 39999.

+1

And I think it'd be good to also
add a protocol option, like _pq_.test_protocol_breakage and fail the
connection attempt client side if that does not get returned back as
unsupported.

Yeah, I feel better signing on to a plan like this if we think the
breakage is likely to lead to a comprehensive fix for
NegotiateProtocolVersion (as opposed to "we greased this thing in 2025
and now this thing in 2026 and now...").

Thanks,
--Jacob

#6Jelte Fennema-Nio
postgres@jeltef.nl
In reply to: Jacob Champion (#5)
2 attachment(s)
Re: libpq: Bump protocol version to version 3.2 at least until the first/second beta

On Fri Oct 31, 2025 at 11:52 PM CET, Jacob Champion wrote:

On Fri, Oct 31, 2025 at 2:56 PM Jelte Fennema-Nio <me@jeltef.nl> wrote:
Yeah, I feel better signing on to a plan like this if we think the
breakage is likely to lead to a comprehensive fix for
NegotiateProtocolVersion (as opposed to "we greased this thing in 2025
and now this thing in 2026 and now...").

Attached is a new version of the patch that reserves version 3.9999 and
_pq_.test_protocol_negotiation and starts requesting those by default
(which we should revert before at least PG19rc1 and possibly earlier).

Attachments:

v3-0001-Add-test-for-libpq-its-default-protocol-version.patchtext/x-patch; charset=utf-8; name=v3-0001-Add-test-for-libpq-its-default-protocol-version.patchDownload
From 51fefaabed1b56aae891d37d91e9891382a9d659 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Thu, 23 Oct 2025 15:16:05 +0200
Subject: [PATCH v3 1/2] Add test for libpq its default protocol version

We did not test libpq its default protocol version. Defaults are
important so we should test them.
---
 .../modules/libpq_pipeline/libpq_pipeline.c   | 33 ++++++++++++++++---
 1 file changed, 28 insertions(+), 5 deletions(-)

diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c
index b3af70fa09b..3af79e03d24 100644
--- a/src/test/modules/libpq_pipeline/libpq_pipeline.c
+++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c
@@ -1328,7 +1328,7 @@ test_protocol_version(PGconn *conn)
 	int			nopts;
 	PQconninfoOption *opts = PQconninfo(conn);
 	int			protocol_version;
-	int			max_protocol_version_index;
+	int			max_protocol_version_index = -1;
 	int			i;
 
 	/*
@@ -1351,14 +1351,37 @@ test_protocol_version(PGconn *conn)
 		{
 			keywords[i] = opt->keyword;
 			vals[i] = opt->val;
+			if (strcmp(opt->keyword, "max_protocol_version") == 0)
+			{
+				max_protocol_version_index = i;
+			}
+
 			i++;
 		}
 	}
 
-	max_protocol_version_index = i;
-	keywords[i] = "max_protocol_version";	/* value is filled in below */
-	i++;
-	keywords[i] = vals[i] = NULL;
+	if (max_protocol_version_index == -1)
+	{
+		max_protocol_version_index = i;
+		keywords[i] = "max_protocol_version";	/* value is filled in below */
+		i++;
+	}
+
+	/*
+	 * Test default protocol_version
+	 */
+	vals[max_protocol_version_index] = "";
+	conn = PQconnectdbParams(keywords, vals, false);
+
+	if (PQstatus(conn) != CONNECTION_OK)
+		pg_fatal("Connection to database failed: %s",
+				 PQerrorMessage(conn));
+
+	protocol_version = PQfullProtocolVersion(conn);
+	if (protocol_version != 30000)
+		pg_fatal("expected 30000, got %d", protocol_version);
+
+	PQfinish(conn);
 
 	/*
 	 * Test max_protocol_version=3.0

base-commit: ad25744f436ed7809fc754e1a44630b087812fbc
-- 
2.51.1

v3-0002-libpq-Request-protocol-version-3.9999-to-GREASE-t.patchtext/x-patch; charset=utf-8; name=v3-0002-libpq-Request-protocol-version-3.9999-to-GREASE-t.patchDownload
From 31851ddff8cb40f732aac0bfac364da61ed9fa30 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Thu, 23 Oct 2025 15:08:52 +0200
Subject: [PATCH v3 2/2] libpq: Request protocol version 3.9999 to GREASE the
 ecosystem

The main reason that libpq does not request protocol version 3.2 by
default is because other proxy/server implementations don't implement
the negotiation. This is a bit of a chicken and egg problem: We don't
bump the default version that libpq asks, but proxies will only
implement version negotiation when their users run into issues.

This patch defines 3.999 as an explicitly unsupported protocol version
number and _pq_.test_protocol_negotiation as an explicitly unsupported
protocol extension. It also starts requesting that version and protocol
extension by default. This change to the default will be reverted before
we release PG19 release candidates (when exactly to revert before that
time is TBD). The intent is to stress test the ecosystem for
servers/middleware that don't support protocol version negotiation, so
that those servers/middleware can implement the negotiation. This is
similar to the GREASE[1] mechanism that TLS has.

It's still possible for users to connect to servers that don't support
protocol negotiation by using max_protocol_version=3.0 in their
connection string. Only the default connection behaviour is impacted.

[1]: https://www.rfc-editor.org/rfc/rfc8701.html

Author: Jelte Fennema-Nio <postgres@jeltef.nl>
---
 doc/src/sgml/libpq.sgml                       | 24 ++++++----
 doc/src/sgml/protocol.sgml                    | 43 +++++++++++++++--
 src/include/libpq/pqcomm.h                    |  7 +++
 src/interfaces/libpq/fe-connect.c             | 20 ++++----
 src/interfaces/libpq/fe-protocol3.c           | 46 +++++++++++++++++--
 .../modules/libpq_pipeline/libpq_pipeline.c   |  6 +--
 6 files changed, 119 insertions(+), 27 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index e76da383714..6b1e4625d17 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2213,15 +2213,21 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       <listitem>
        <para>
         Specifies the protocol version to request from the server.
-        The default is to use version <literal>3.0</literal> of the
-        <productname>PostgreSQL</productname> protocol, unless the connection
-        string specifies a feature that relies on a higher protocol version,
-        in which case the latest version supported by libpq is used. If the
-        server does not support the protocol version requested by the client,
-        the connection is automatically downgraded to a lower minor protocol
-        version that the server supports. After the connection attempt has
-        completed you can use <xref linkend="libpq-PQfullProtocolVersion"/> to
-        find out which exact protocol version was negotiated.
+        During the PostgreSQL 19 beta period, the default is to use
+        <literal>3.9999</literal>, a GREASE (Generate Random Extensions And
+        Sustain Extensibility) value that tests proper protocol negotiation
+        implementation. If the server does not support the protocol version
+        requested by the client, the connection is automatically downgraded to
+        a lower minor protocol version that the server supports. After the
+        connection attempt has completed you can use
+        <xref linkend="libpq-PQfullProtocolVersion"/> to find out which exact
+        protocol version was negotiated.
+       </para>
+
+       <para>
+        For servers that don't properly implement protocol version negotiation,
+        you can set <literal>max_protocol_version=3.0</literal> to connect
+        successfully.
        </para>
 
        <para>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 9d755232873..66c540c6029 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -192,10 +192,13 @@
    <title>Protocol Versions</title>
 
    <para>
-    The current, latest version of the protocol is version 3.2. However, for
-    backwards compatibility with old server versions and middleware that don't
-    support the version negotiation yet, libpq still uses protocol version 3.0
-    by default.
+    The current, latest version of the protocol is version 3.2. During the
+    PostgreSQL 19 beta period, libpq defaults to requesting protocol version
+    3.9999 to test that servers and middleware properly implement protocol
+    version negotiation. Servers that support negotiation will automatically
+    downgrade to version 3.2 or 3.0. For servers that don't support
+    negotiation, users can connect by explicitly setting
+    <literal>max_protocol_version=3.0</literal> in their connection string.
    </para>
 
    <para>
@@ -238,6 +241,20 @@
      </thead>
 
      <tbody>
+      <row>
+      <entry>3.9999</entry>
+      <entry>-</entry>
+      <entry>GREASE (Generate Random Extensions And Sustain Extensibility)
+        version. This version number is intentionally reserved and will never
+        be implemented. During the PostgreSQL 19 beta period, libpq requests
+        this version by default to test that servers and middleware properly
+        implement protocol version negotiation via
+        <literal>NegotiateProtocolVersion</literal>. Servers should respond
+        by downgrading to a supported version. This mechanism helps ensure
+        the ecosystem is ready for future protocol versions. libpq will revert
+        to defaulting to version 3.2 before the PostgreSQL 19 final release.
+      </entry>
+      </row>
       <row>
       <entry>3.2</entry>
       <entry>PostgreSQL 18 and later</entry>
@@ -6148,6 +6165,24 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
             </para>
            </listitem>
           </varlistentry>
+          <varlistentry>
+           <term><literal>_pq_.test_protocol_negotiation</literal></term>
+           <listitem>
+            <para>
+             A reserved protocol extension requested by libpq during the
+             PostgreSQL 19 beta period to test that servers properly implement
+             protocol version negotiation. When the client requests the GREASE
+             protocol version (3.9999), this parameter is automatically
+             included in the startup packet too. Servers should report it as
+             unsupported in their <literal>NegotiateProtocolVersion</literal>
+             response. In GREASE mode the connection will fail if the server
+             doesn't report this parameter as unsupported, ensuring
+             comprehensive implementation of protocol negotiation. This
+             parameter is reserved and will never actually be implemented by a
+             server.
+            </para>
+           </listitem>
+          </varlistentry>
          </variablelist>
 
          In addition to the above, other parameters may be listed.
diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index f04ca135653..4c20105e998 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -91,10 +91,17 @@ is_unixsock_path(const char *path)
 
 /*
  * The earliest and latest frontend/backend protocol version supported.
+ *
+ * PG_PROTOCOL_GREASE is an intentionally unsupported protocol version used
+ * for GREASE (Generate Random Extensions And Sustain Extensibility). This
+ * helps ensure that servers properly implement protocol version negotiation
+ * via NegotiateProtocolVersion. Version 3.9999 was chosen to be safely within
+ * the valid range but unlikely to ever be implemented.
  */
 
 #define PG_PROTOCOL_EARLIEST	PG_PROTOCOL(3,0)
 #define PG_PROTOCOL_LATEST		PG_PROTOCOL(3,2)
+#define PG_PROTOCOL_GREASE		PG_PROTOCOL(3,9999)
 
 typedef uint32 ProtocolVersion; /* FE/BE protocol version number */
 
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index a3d12931fff..e8306d567c1 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -2131,15 +2131,12 @@ pqConnectOptions2(PGconn *conn)
 	else
 	{
 		/*
-		 * To not break connecting to older servers/poolers that do not yet
-		 * support NegotiateProtocolVersion, default to the 3.0 protocol at
-		 * least for a while longer. Except when min_protocol_version is set
-		 * to something larger, then we might as well default to the latest.
+		 * Default to the GREASE protocol version to test that servers
+		 * properly implement NegotiateProtocolVersion. The server will
+		 * automatically downgrade to a supported version. This will be
+		 * changed to a supported version before the PG19 release.
 		 */
-		if (conn->min_pversion > PG_PROTOCOL(3, 0))
-			conn->max_pversion = PG_PROTOCOL_LATEST;
-		else
-			conn->max_pversion = PG_PROTOCOL(3, 0);
+		conn->max_pversion = PG_PROTOCOL_GREASE;
 	}
 
 	if (conn->min_pversion > conn->max_pversion)
@@ -4375,6 +4372,13 @@ keep_going:						/* We will come back to here until there is
 					goto error_return;
 				}
 
+				if (conn->max_pversion == PG_PROTOCOL_GREASE &&
+					conn->pversion == PG_PROTOCOL_GREASE)
+				{
+					libpq_append_conn_error(conn, "server incorrectly accepted reserved GREASE protocol version 3.9999 without negotiation");
+					goto error_return;
+				}
+
 				/* Almost there now ... */
 				conn->status = CONNECTION_CHECK_TARGET;
 				goto keep_going;
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index da7a8db68c8..720bc27bb5d 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -1429,6 +1429,8 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 {
 	int			their_version;
 	int			num;
+	bool		found_test_protocol_negotiation;
+	bool		expect_test_protocol_negotiation;
 
 	if (pqGetInt(&their_version, 4, conn) != 0)
 		goto eof;
@@ -1456,6 +1458,13 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 		goto failure;
 	}
 
+	/* The GREASE protocol version is intentionally unsupported and reserved */
+	if (their_version == PG_PROTOCOL_GREASE)
+	{
+		libpq_append_conn_error(conn, "received invalid protocol negotiation message: server claimed to support reserved GREASE protocol version 3.9999");
+		goto failure;
+	}
+
 	if (num < 0)
 	{
 		libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported negative number of unsupported parameters");
@@ -1484,9 +1493,12 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 	conn->pversion = their_version;
 
 	/*
-	 * We don't currently request any protocol extensions, so we don't expect
-	 * the server to reply with any either.
+	 * Check that all expected unsupported parameters are reported by the
+	 * server.
 	 */
+	found_test_protocol_negotiation = false;
+	expect_test_protocol_negotiation = (conn->max_pversion == PG_PROTOCOL_GREASE);
+
 	for (int i = 0; i < num; i++)
 	{
 		if (pqGets(&conn->workBuffer, conn))
@@ -1498,7 +1510,27 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 			libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported unsupported parameter name without a \"%s\" prefix (\"%s\")", "_pq_.", conn->workBuffer.data);
 			goto failure;
 		}
-		libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported an unsupported parameter that was not requested (\"%s\")", conn->workBuffer.data);
+
+		/* Check if this is the expected test parameter */
+		if (expect_test_protocol_negotiation && strcmp(conn->workBuffer.data, "_pq_.test_protocol_negotiation") == 0)
+		{
+			found_test_protocol_negotiation = true;
+		}
+		else
+		{
+			libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported an unsupported parameter that was not requested (\"%s\")", conn->workBuffer.data);
+			goto failure;
+		}
+	}
+
+	/*
+	 * If we requested the GREASE protocol version, the server must report
+	 * _pq_.test_protocol_negotiation as unsupported. This ensures
+	 * comprehensive NegotiateProtocolVersion implementation.
+	 */
+	if (expect_test_protocol_negotiation && !found_test_protocol_negotiation)
+	{
+		libpq_append_conn_error(conn, "server did not report the unsupported `_pq_.test_protocol_negotiation` parameter in its protocol negotiation message");
 		goto failure;
 	}
 
@@ -2439,6 +2471,14 @@ build_startup_packet(const PGconn *conn, char *packet,
 	if (conn->client_encoding_initial && conn->client_encoding_initial[0])
 		ADD_STARTUP_OPTION("client_encoding", conn->client_encoding_initial);
 
+	/*
+	 * Add the test protocol negotiation option if we're using the GREASE
+	 * protocol version. This tests that servers properly report unsupported
+	 * protocol options in their NegotiateProtocolVersion response.
+	 */
+	if (conn->pversion == PG_PROTOCOL_GREASE)
+		ADD_STARTUP_OPTION("_pq_.test_protocol_negotiation", "");
+
 	/* Add any environment-driven GUC settings needed */
 	for (next_eo = options; next_eo->envName; next_eo++)
 	{
diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c
index 3af79e03d24..2372a77908b 100644
--- a/src/test/modules/libpq_pipeline/libpq_pipeline.c
+++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c
@@ -1368,7 +1368,7 @@ test_protocol_version(PGconn *conn)
 	}
 
 	/*
-	 * Test default protocol_version
+	 * Test default protocol_version (GREASE - should negotiate down to 3.2)
 	 */
 	vals[max_protocol_version_index] = "";
 	conn = PQconnectdbParams(keywords, vals, false);
@@ -1378,8 +1378,8 @@ test_protocol_version(PGconn *conn)
 				 PQerrorMessage(conn));
 
 	protocol_version = PQfullProtocolVersion(conn);
-	if (protocol_version != 30000)
-		pg_fatal("expected 30000, got %d", protocol_version);
+	if (protocol_version != 30002)
+		pg_fatal("expected 30002, got %d", protocol_version);
 
 	PQfinish(conn);
 
-- 
2.51.1

#7Andres Freund
andres@anarazel.de
In reply to: Jelte Fennema-Nio (#6)
Re: libpq: Bump protocol version to version 3.2 at least until the first/second beta

Hi,

On 2025-11-03 15:46:10 +0100, Jelte Fennema-Nio wrote:

From 31851ddff8cb40f732aac0bfac364da61ed9fa30 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Thu, 23 Oct 2025 15:08:52 +0200
Subject: [PATCH v3 2/2] libpq: Request protocol version 3.9999 to GREASE the
ecosystem

The main reason that libpq does not request protocol version 3.2 by
default is because other proxy/server implementations don't implement
the negotiation. This is a bit of a chicken and egg problem: We don't
bump the default version that libpq asks, but proxies will only
implement version negotiation when their users run into issues.

This patch defines 3.999 as an explicitly unsupported protocol version
number and _pq_.test_protocol_negotiation as an explicitly unsupported
protocol extension. It also starts requesting that version and protocol
extension by default. This change to the default will be reverted before
we release PG19 release candidates (when exactly to revert before that
time is TBD). The intent is to stress test the ecosystem for
servers/middleware that don't support protocol version negotiation, so
that those servers/middleware can implement the negotiation. This is
similar to the GREASE[1] mechanism that TLS has.

It's still possible for users to connect to servers that don't support
protocol negotiation by using max_protocol_version=3.0 in their
connection string. Only the default connection behaviour is impacted.

[1]: https://www.rfc-editor.org/rfc/rfc8701.html

Won't this mean that it'll be harder to performance comparisons between the
in-development version and other versions? Because there will be negotiation
before we branch of 19, but not after and not in release branches?

Greetings,

Andres Freund

#8Jelte Fennema-Nio
postgres@jeltef.nl
In reply to: Andres Freund (#7)
Re: libpq: Bump protocol version to version 3.2 at least until the first/second beta

On Mon, 3 Nov 2025 at 15:59, Andres Freund <andres@anarazel.de> wrote:

Won't this mean that it'll be harder to performance comparisons between the
in-development version and other versions? Because there will be negotiation
before we branch of 19, but not after and not in release branches?

The negotiation does not require a separate roundtrip, only a tiny
additional message sent by the server. So I'm not worried about that
resulting in a measurable perf change. And even if there is one in
some super extreme benchmark, then you can still set
max_protocol_version=3.0 to revert to the regular behaviour.

#9Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Jelte Fennema-Nio (#8)
5 attachment(s)
Re: libpq: Bump protocol version to version 3.2 at least until the first/second beta

On Mon, Nov 3, 2025 at 7:42 AM Jelte Fennema-Nio <postgres@jeltef.nl> wrote:

The negotiation does not require a separate roundtrip, only a tiny
additional message sent by the server. So I'm not worried about that
resulting in a measurable perf change. And even if there is one in
some super extreme benchmark, then you can still set
max_protocol_version=3.0 to revert to the regular behaviour.

Andres, should I take from the silence that you're satisfied with that?

On Mon, Nov 3, 2025 at 6:46 AM Jelte Fennema-Nio <postgres@jeltef.nl> wrote:

Attached is a new version of the patch that reserves version 3.9999 and
_pq_.test_protocol_negotiation and starts requesting those by default
(which we should revert before at least PG19rc1 and possibly earlier).

Partial review follows, in a v4 squash! set, as requested on the Discord :)

v4-0001 and 0002 should be identical to v3, modulo any rebased diff context.

v4-0003: I was initially confused about the need to change the
max_protocol_version_index logic in v3-0001 (it's because empty
parameter values are ignored by PQconnectdbParams). Then I decided
that if we have to maintain the logic to overwrite
max_protocol_version in-place, we might as well always use it. v4-0003
does that and adds some explanation.

v4-0004 does a cosmetic move of the PG_PROTOCOL_GREASE macro now that
we have a section for it.

v4-0005 is a temporary distraction to move protocol version 3.1 into
the same place. If people don't like it I'm happy to drop it for now
(it may deserve at least some bikeshedding on the macro name, since
it's part of a public header).

= Additional Thoughts =

I want to more clearly decouple ourselves from TLS's GREASE in the
documentation and comments. We aren't "Generating Random Extensions"
(we _could_, but that takes a lot more thought), nor are we telling
OpenSSL to enable GREASE for our TLS connections. It's fine if we want
to gesture in that direction as broader context, but I don't want to
cause user confusion. I'll work on some proposed changes for that.

I'd like reserve a (protected?) wiki page, or something of the sort,
that we can point people to directly if they hit any grease failures.
"Server screwed up" is probably not enough context for a typical user
to know what to do next.

I will also work on splitting 0002 into revertable and not-revertable
halves. The grease constant probably needs to remain documented and
reserved even if it doesn't do anything for 19.0.

Finally: is there any appetite for retaining the ability to grease
connections as production functionality, e.g. via
`max_protocol_version=grease`? Personally I think it'd be nice, but
it's not a trivial amount of extra work. We'd have to handle the case
where a future server responds with a legitimate minor version that's
newer than what our version of libpq supports. And I think we'd want a
production-grade version of this to add some randomization tricks, to
discourage people from keying on grease constants.

Thanks,
--Jacob

Attachments:

v4-0001-Add-test-for-libpq-its-default-protocol-version.patchapplication/octet-stream; name=v4-0001-Add-test-for-libpq-its-default-protocol-version.patchDownload
From 10980c45f1d3cbbbe8c0df8f739fc83ce89233b2 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Thu, 23 Oct 2025 15:16:05 +0200
Subject: [PATCH v4 1/5] Add test for libpq its default protocol version

We did not test libpq its default protocol version. Defaults are
important so we should test them.
---
 .../modules/libpq_pipeline/libpq_pipeline.c   | 33 ++++++++++++++++---
 1 file changed, 28 insertions(+), 5 deletions(-)

diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c
index 03371721460..dea94db6ea1 100644
--- a/src/test/modules/libpq_pipeline/libpq_pipeline.c
+++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c
@@ -1328,7 +1328,7 @@ test_protocol_version(PGconn *conn)
 	int			nopts;
 	PQconninfoOption *opts = PQconninfo(conn);
 	int			protocol_version;
-	int			max_protocol_version_index;
+	int			max_protocol_version_index = -1;
 	int			i;
 
 	/*
@@ -1351,14 +1351,37 @@ test_protocol_version(PGconn *conn)
 		{
 			keywords[i] = opt->keyword;
 			vals[i] = opt->val;
+			if (strcmp(opt->keyword, "max_protocol_version") == 0)
+			{
+				max_protocol_version_index = i;
+			}
+
 			i++;
 		}
 	}
 
-	max_protocol_version_index = i;
-	keywords[i] = "max_protocol_version";	/* value is filled in below */
-	i++;
-	keywords[i] = vals[i] = NULL;
+	if (max_protocol_version_index == -1)
+	{
+		max_protocol_version_index = i;
+		keywords[i] = "max_protocol_version";	/* value is filled in below */
+		i++;
+	}
+
+	/*
+	 * Test default protocol_version
+	 */
+	vals[max_protocol_version_index] = "";
+	conn = PQconnectdbParams(keywords, vals, false);
+
+	if (PQstatus(conn) != CONNECTION_OK)
+		pg_fatal("Connection to database failed: %s",
+				 PQerrorMessage(conn));
+
+	protocol_version = PQfullProtocolVersion(conn);
+	if (protocol_version != 30000)
+		pg_fatal("expected 30000, got %d", protocol_version);
+
+	PQfinish(conn);
 
 	/*
 	 * Test max_protocol_version=3.0
-- 
2.34.1

v4-0002-libpq-Request-protocol-version-3.9999-to-GREASE-t.patchapplication/octet-stream; name=v4-0002-libpq-Request-protocol-version-3.9999-to-GREASE-t.patchDownload
From f62485d8523162162d708243752aab6471984285 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Thu, 23 Oct 2025 15:08:52 +0200
Subject: [PATCH v4 2/5] libpq: Request protocol version 3.9999 to GREASE the
 ecosystem

The main reason that libpq does not request protocol version 3.2 by
default is because other proxy/server implementations don't implement
the negotiation. This is a bit of a chicken and egg problem: We don't
bump the default version that libpq asks, but proxies will only
implement version negotiation when their users run into issues.

This patch defines 3.999 as an explicitly unsupported protocol version
number and _pq_.test_protocol_negotiation as an explicitly unsupported
protocol extension. It also starts requesting that version and protocol
extension by default. This change to the default will be reverted before
we release PG19 release candidates (when exactly to revert before that
time is TBD). The intent is to stress test the ecosystem for
servers/middleware that don't support protocol version negotiation, so
that those servers/middleware can implement the negotiation. This is
similar to the GREASE[1] mechanism that TLS has.

It's still possible for users to connect to servers that don't support
protocol negotiation by using max_protocol_version=3.0 in their
connection string. Only the default connection behaviour is impacted.

[1]: https://www.rfc-editor.org/rfc/rfc8701.html

Author: Jelte Fennema-Nio <postgres@jeltef.nl>
---
 doc/src/sgml/libpq.sgml                       | 24 ++++++----
 doc/src/sgml/protocol.sgml                    | 43 +++++++++++++++--
 src/include/libpq/pqcomm.h                    |  7 +++
 src/interfaces/libpq/fe-connect.c             | 20 ++++----
 src/interfaces/libpq/fe-protocol3.c           | 46 +++++++++++++++++--
 .../modules/libpq_pipeline/libpq_pipeline.c   |  6 +--
 6 files changed, 119 insertions(+), 27 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 7d05938feda..49af462eb29 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2213,15 +2213,21 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       <listitem>
        <para>
         Specifies the protocol version to request from the server.
-        The default is to use version <literal>3.0</literal> of the
-        <productname>PostgreSQL</productname> protocol, unless the connection
-        string specifies a feature that relies on a higher protocol version,
-        in which case the latest version supported by libpq is used. If the
-        server does not support the protocol version requested by the client,
-        the connection is automatically downgraded to a lower minor protocol
-        version that the server supports. After the connection attempt has
-        completed you can use <xref linkend="libpq-PQfullProtocolVersion"/> to
-        find out which exact protocol version was negotiated.
+        During the PostgreSQL 19 beta period, the default is to use
+        <literal>3.9999</literal>, a GREASE (Generate Random Extensions And
+        Sustain Extensibility) value that tests proper protocol negotiation
+        implementation. If the server does not support the protocol version
+        requested by the client, the connection is automatically downgraded to
+        a lower minor protocol version that the server supports. After the
+        connection attempt has completed you can use
+        <xref linkend="libpq-PQfullProtocolVersion"/> to find out which exact
+        protocol version was negotiated.
+       </para>
+
+       <para>
+        For servers that don't properly implement protocol version negotiation,
+        you can set <literal>max_protocol_version=3.0</literal> to connect
+        successfully.
        </para>
 
        <para>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 41c5954a424..f24bf06b87b 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -192,10 +192,13 @@
    <title>Protocol Versions</title>
 
    <para>
-    The current, latest version of the protocol is version 3.2. However, for
-    backwards compatibility with old server versions and middleware that don't
-    support the version negotiation yet, libpq still uses protocol version 3.0
-    by default.
+    The current, latest version of the protocol is version 3.2. During the
+    PostgreSQL 19 beta period, libpq defaults to requesting protocol version
+    3.9999 to test that servers and middleware properly implement protocol
+    version negotiation. Servers that support negotiation will automatically
+    downgrade to version 3.2 or 3.0. For servers that don't support
+    negotiation, users can connect by explicitly setting
+    <literal>max_protocol_version=3.0</literal> in their connection string.
    </para>
 
    <para>
@@ -238,6 +241,20 @@
      </thead>
 
      <tbody>
+      <row>
+      <entry>3.9999</entry>
+      <entry>-</entry>
+      <entry>GREASE (Generate Random Extensions And Sustain Extensibility)
+        version. This version number is intentionally reserved and will never
+        be implemented. During the PostgreSQL 19 beta period, libpq requests
+        this version by default to test that servers and middleware properly
+        implement protocol version negotiation via
+        <literal>NegotiateProtocolVersion</literal>. Servers should respond
+        by downgrading to a supported version. This mechanism helps ensure
+        the ecosystem is ready for future protocol versions. libpq will revert
+        to defaulting to version 3.2 before the PostgreSQL 19 final release.
+      </entry>
+      </row>
       <row>
       <entry>3.2</entry>
       <entry>PostgreSQL 18 and later</entry>
@@ -6148,6 +6165,24 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
             </para>
            </listitem>
           </varlistentry>
+          <varlistentry>
+           <term><literal>_pq_.test_protocol_negotiation</literal></term>
+           <listitem>
+            <para>
+             A reserved protocol extension requested by libpq during the
+             PostgreSQL 19 beta period to test that servers properly implement
+             protocol version negotiation. When the client requests the GREASE
+             protocol version (3.9999), this parameter is automatically
+             included in the startup packet too. Servers should report it as
+             unsupported in their <literal>NegotiateProtocolVersion</literal>
+             response. In GREASE mode the connection will fail if the server
+             doesn't report this parameter as unsupported, ensuring
+             comprehensive implementation of protocol negotiation. This
+             parameter is reserved and will never actually be implemented by a
+             server.
+            </para>
+           </listitem>
+          </varlistentry>
          </variablelist>
 
          In addition to the above, other parameters may be listed.
diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index 6df6e0f902d..a4d04e79f9f 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -90,9 +90,16 @@ is_unixsock_path(const char *path)
 
 /*
  * The earliest and latest frontend/backend protocol version supported.
+ *
+ * PG_PROTOCOL_GREASE is an intentionally unsupported protocol version used
+ * for GREASE (Generate Random Extensions And Sustain Extensibility). This
+ * helps ensure that servers properly implement protocol version negotiation
+ * via NegotiateProtocolVersion. Version 3.9999 was chosen to be safely within
+ * the valid range but unlikely to ever be implemented.
  */
 #define PG_PROTOCOL_EARLIEST	PG_PROTOCOL(3,0)
 #define PG_PROTOCOL_LATEST		PG_PROTOCOL(3,2)
+#define PG_PROTOCOL_GREASE		PG_PROTOCOL(3,9999)
 
 /*
  * Reserved protocol numbers, which have special semantics:
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index a0d2f749811..9fcf094a36f 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -2142,15 +2142,12 @@ pqConnectOptions2(PGconn *conn)
 	else
 	{
 		/*
-		 * To not break connecting to older servers/poolers that do not yet
-		 * support NegotiateProtocolVersion, default to the 3.0 protocol at
-		 * least for a while longer. Except when min_protocol_version is set
-		 * to something larger, then we might as well default to the latest.
+		 * Default to the GREASE protocol version to test that servers
+		 * properly implement NegotiateProtocolVersion. The server will
+		 * automatically downgrade to a supported version. This will be
+		 * changed to a supported version before the PG19 release.
 		 */
-		if (conn->min_pversion > PG_PROTOCOL(3, 0))
-			conn->max_pversion = PG_PROTOCOL_LATEST;
-		else
-			conn->max_pversion = PG_PROTOCOL(3, 0);
+		conn->max_pversion = PG_PROTOCOL_GREASE;
 	}
 
 	if (conn->min_pversion > conn->max_pversion)
@@ -4386,6 +4383,13 @@ keep_going:						/* We will come back to here until there is
 					goto error_return;
 				}
 
+				if (conn->max_pversion == PG_PROTOCOL_GREASE &&
+					conn->pversion == PG_PROTOCOL_GREASE)
+				{
+					libpq_append_conn_error(conn, "server incorrectly accepted reserved GREASE protocol version 3.9999 without negotiation");
+					goto error_return;
+				}
+
 				/* Almost there now ... */
 				conn->status = CONNECTION_CHECK_TARGET;
 				goto keep_going;
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 34518bbe6ea..7457ba2498c 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -1444,6 +1444,8 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 {
 	int			their_version;
 	int			num;
+	bool		found_test_protocol_negotiation;
+	bool		expect_test_protocol_negotiation;
 
 	if (pqGetInt(&their_version, 4, conn) != 0)
 		goto eof;
@@ -1471,6 +1473,13 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 		goto failure;
 	}
 
+	/* The GREASE protocol version is intentionally unsupported and reserved */
+	if (their_version == PG_PROTOCOL_GREASE)
+	{
+		libpq_append_conn_error(conn, "received invalid protocol negotiation message: server claimed to support reserved GREASE protocol version 3.9999");
+		goto failure;
+	}
+
 	if (num < 0)
 	{
 		libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported negative number of unsupported parameters");
@@ -1499,9 +1508,12 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 	conn->pversion = their_version;
 
 	/*
-	 * We don't currently request any protocol extensions, so we don't expect
-	 * the server to reply with any either.
+	 * Check that all expected unsupported parameters are reported by the
+	 * server.
 	 */
+	found_test_protocol_negotiation = false;
+	expect_test_protocol_negotiation = (conn->max_pversion == PG_PROTOCOL_GREASE);
+
 	for (int i = 0; i < num; i++)
 	{
 		if (pqGets(&conn->workBuffer, conn))
@@ -1513,7 +1525,27 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 			libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported unsupported parameter name without a \"%s\" prefix (\"%s\")", "_pq_.", conn->workBuffer.data);
 			goto failure;
 		}
-		libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported an unsupported parameter that was not requested (\"%s\")", conn->workBuffer.data);
+
+		/* Check if this is the expected test parameter */
+		if (expect_test_protocol_negotiation && strcmp(conn->workBuffer.data, "_pq_.test_protocol_negotiation") == 0)
+		{
+			found_test_protocol_negotiation = true;
+		}
+		else
+		{
+			libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported an unsupported parameter that was not requested (\"%s\")", conn->workBuffer.data);
+			goto failure;
+		}
+	}
+
+	/*
+	 * If we requested the GREASE protocol version, the server must report
+	 * _pq_.test_protocol_negotiation as unsupported. This ensures
+	 * comprehensive NegotiateProtocolVersion implementation.
+	 */
+	if (expect_test_protocol_negotiation && !found_test_protocol_negotiation)
+	{
+		libpq_append_conn_error(conn, "server did not report the unsupported `_pq_.test_protocol_negotiation` parameter in its protocol negotiation message");
 		goto failure;
 	}
 
@@ -2464,6 +2496,14 @@ build_startup_packet(const PGconn *conn, char *packet,
 	if (conn->client_encoding_initial && conn->client_encoding_initial[0])
 		ADD_STARTUP_OPTION("client_encoding", conn->client_encoding_initial);
 
+	/*
+	 * Add the test protocol negotiation option if we're using the GREASE
+	 * protocol version. This tests that servers properly report unsupported
+	 * protocol options in their NegotiateProtocolVersion response.
+	 */
+	if (conn->pversion == PG_PROTOCOL_GREASE)
+		ADD_STARTUP_OPTION("_pq_.test_protocol_negotiation", "");
+
 	/* Add any environment-driven GUC settings needed */
 	for (next_eo = options; next_eo->envName; next_eo++)
 	{
diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c
index dea94db6ea1..dd99fcb3667 100644
--- a/src/test/modules/libpq_pipeline/libpq_pipeline.c
+++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c
@@ -1368,7 +1368,7 @@ test_protocol_version(PGconn *conn)
 	}
 
 	/*
-	 * Test default protocol_version
+	 * Test default protocol_version (GREASE - should negotiate down to 3.2)
 	 */
 	vals[max_protocol_version_index] = "";
 	conn = PQconnectdbParams(keywords, vals, false);
@@ -1378,8 +1378,8 @@ test_protocol_version(PGconn *conn)
 				 PQerrorMessage(conn));
 
 	protocol_version = PQfullProtocolVersion(conn);
-	if (protocol_version != 30000)
-		pg_fatal("expected 30000, got %d", protocol_version);
+	if (protocol_version != 30002)
+		pg_fatal("expected 30002, got %d", protocol_version);
 
 	PQfinish(conn);
 
-- 
2.34.1

v4-0003-squash-Add-test-for-libpq-its-default-protocol-ve.patchapplication/octet-stream; name=v4-0003-squash-Add-test-for-libpq-its-default-protocol-ve.patchDownload
From 1476c27256b5da1e823445ff649ff3b71ca70dc0 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 21 Nov 2025 16:42:22 -0800
Subject: [PATCH v4 3/5] squash! Add test for libpq its default protocol
 version

- explain why we're setting max_protocol_version in-place now
- condense the logic so that we *always* set max_protocol_version
  in-place
---
 .../modules/libpq_pipeline/libpq_pipeline.c   | 35 ++++++++-----------
 1 file changed, 15 insertions(+), 20 deletions(-)

diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c
index dd99fcb3667..b819bcc273c 100644
--- a/src/test/modules/libpq_pipeline/libpq_pipeline.c
+++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c
@@ -1331,14 +1331,10 @@ test_protocol_version(PGconn *conn)
 	int			max_protocol_version_index = -1;
 	int			i;
 
-	/*
-	 * Prepare keywords/vals arrays, copied from the existing connection, with
-	 * an extra slot for 'max_protocol_version'.
-	 */
+	/* Prepare keywords/vals arrays, copied from the existing connection. */
 	nopts = 0;
 	for (PQconninfoOption *opt = opts; opt->keyword != NULL; ++opt)
 		nopts++;
-	nopts++;					/* max_protocol_version */
 	nopts++;					/* NULL terminator */
 
 	keywords = pg_malloc0(sizeof(char *) * nopts);
@@ -1347,26 +1343,25 @@ test_protocol_version(PGconn *conn)
 	i = 0;
 	for (PQconninfoOption *opt = opts; opt->keyword != NULL; ++opt)
 	{
-		if (opt->val)
-		{
-			keywords[i] = opt->keyword;
-			vals[i] = opt->val;
-			if (strcmp(opt->keyword, "max_protocol_version") == 0)
-			{
-				max_protocol_version_index = i;
-			}
+		/*
+		 * If the test already specified max_protocol_version, we want to
+		 * replace it rather than attempting to override it. This matters when
+		 * testing defaults, because empty option values at the end of the
+		 * connection string won't replace earlier settings.
+		 */
+		if (strcmp(opt->keyword, "max_protocol_version") == 0)
+			max_protocol_version_index = i;
+		else if (!opt->val)
+			continue;
 
-			i++;
-		}
-	}
+		keywords[i] = opt->keyword;
+		vals[i] = opt->val;
 
-	if (max_protocol_version_index == -1)
-	{
-		max_protocol_version_index = i;
-		keywords[i] = "max_protocol_version";	/* value is filled in below */
 		i++;
 	}
 
+	Assert(max_protocol_version_index >= 0);
+
 	/*
 	 * Test default protocol_version (GREASE - should negotiate down to 3.2)
 	 */
-- 
2.34.1

v4-0004-squash-libpq-Request-protocol-version-3.9999-to-G.patchapplication/octet-stream; name=v4-0004-squash-libpq-Request-protocol-version-3.9999-to-G.patchDownload
From 78960f566a66d1d4aefb4e451d36671889d02d8e Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 21 Nov 2025 10:44:31 -0800
Subject: [PATCH v4 4/5] squash! libpq: Request protocol version 3.9999 to
 GREASE the ecosystem

Move PG_PROTOCOL_GREASE into the reserved section of the header.
---
 src/include/libpq/pqcomm.h | 16 +++++++++-------
 1 file changed, 9 insertions(+), 7 deletions(-)

diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index a4d04e79f9f..0fe7d8e897d 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -90,21 +90,23 @@ is_unixsock_path(const char *path)
 
 /*
  * The earliest and latest frontend/backend protocol version supported.
- *
- * PG_PROTOCOL_GREASE is an intentionally unsupported protocol version used
- * for GREASE (Generate Random Extensions And Sustain Extensibility). This
- * helps ensure that servers properly implement protocol version negotiation
- * via NegotiateProtocolVersion. Version 3.9999 was chosen to be safely within
- * the valid range but unlikely to ever be implemented.
  */
 #define PG_PROTOCOL_EARLIEST	PG_PROTOCOL(3,0)
 #define PG_PROTOCOL_LATEST		PG_PROTOCOL(3,2)
-#define PG_PROTOCOL_GREASE		PG_PROTOCOL(3,9999)
 
 /*
  * Reserved protocol numbers, which have special semantics:
  */
 
+/*
+ * PG_PROTOCOL_GREASE is an intentionally unsupported protocol version used
+ * for GREASE (Generate Random Extensions And Sustain Extensibility). This
+ * helps ensure that servers properly implement protocol version negotiation
+ * via NegotiateProtocolVersion. Version 3.9999 was chosen to be safely within
+ * the valid range but unlikely to ever be implemented.
+ */
+#define PG_PROTOCOL_GREASE		PG_PROTOCOL(3,9999)
+
 /*
  * A client can send a cancel-current-operation request to the postmaster.
  * This is uglier than sending it directly to the client's backend, but it
-- 
2.34.1

v4-0005-pqcomm.h-Explicitly-reserve-protocol-v3.1.patchapplication/octet-stream; name=v4-0005-pqcomm.h-Explicitly-reserve-protocol-v3.1.patchDownload
From edd5d0719c3095a1dbbcefc0ef4c8b2b37d4a2d8 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Fri, 21 Nov 2025 09:18:40 -0800
Subject: [PATCH v4 5/5] pqcomm.h: Explicitly reserve protocol v3.1

Document this alongside the other special protocol numbers.
---
 src/include/libpq/pqcomm.h          | 6 ++++++
 src/interfaces/libpq/fe-protocol3.c | 2 +-
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index 0fe7d8e897d..31577e1cf98 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -98,6 +98,12 @@ is_unixsock_path(const char *path)
  * Reserved protocol numbers, which have special semantics:
  */
 
+/*
+ * 3.1 would have collided with old pgbouncer deployments, and was skipped. We
+ * neither emit it nor accept it on the wire.
+ */
+#define PG_PROTOCOL_RSRV31		PG_PROTOCOL(3,1)
+
 /*
  * PG_PROTOCOL_GREASE is an intentionally unsupported protocol version used
  * for GREASE (Generate Random Extensions And Sustain Extensibility). This
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 7457ba2498c..e32a7a33e32 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -1467,7 +1467,7 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 	}
 
 	/* 3.1 never existed, we went straight from 3.0 to 3.2 */
-	if (their_version == PG_PROTOCOL(3, 1))
+	if (their_version == PG_PROTOCOL_RSRV31)
 	{
 		libpq_append_conn_error(conn, "received invalid protocol negotiation message: server requested downgrade to non-existent 3.1 protocol version");
 		goto failure;
-- 
2.34.1

#10Andres Freund
andres@anarazel.de
In reply to: Jacob Champion (#9)
Re: libpq: Bump protocol version to version 3.2 at least until the first/second beta

On 2026-01-14 12:23:24 -0800, Jacob Champion wrote:

On Mon, Nov 3, 2025 at 7:42 AM Jelte Fennema-Nio <postgres@jeltef.nl> wrote:

The negotiation does not require a separate roundtrip, only a tiny
additional message sent by the server. So I'm not worried about that
resulting in a measurable perf change. And even if there is one in
some super extreme benchmark, then you can still set
max_protocol_version=3.0 to revert to the regular behaviour.

Andres, should I take from the silence that you're satisfied with that?

My concern about that aspect has been ameliorated, I have however not
looked/thought about anything else in this proposal.

#11Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Andres Freund (#10)
Re: libpq: Bump protocol version to version 3.2 at least until the first/second beta

On Wed, Jan 14, 2026 at 1:43 PM Andres Freund <andres@anarazel.de> wrote:

My concern about that aspect has been ameliorated, I have however not
looked/thought about anything else in this proposal.

Excellent, thanks!

--Jacob

#12Jelte Fennema-Nio
postgres@jeltef.nl
In reply to: Jacob Champion (#9)
Re: libpq: Bump protocol version to version 3.2 at least until the first/second beta

On Wed Jan 14, 2026 at 9:23 PM CET, Jacob Champion wrote:

Partial review follows, in a v4 squash! set, as requested on the Discord :)

All changes in those 3 additional patches look totally reasonable to me.

= Additional Thoughts =

I want to more clearly decouple ourselves from TLS's GREASE in the
documentation and comments. We aren't "Generating Random Extensions"
(we _could_, but that takes a lot more thought), nor are we telling
OpenSSL to enable GREASE for our TLS connections. It's fine if we want
to gesture in that direction as broader context, but I don't want to
cause user confusion. I'll work on some proposed changes for that.

Yeah, I didnt't realize that since TLS GREASE it became a broader term.
Definitely seems reasonable to reference the generic grease RFC instead
(which you have shared in the other protocol thread I think). So yeah
feel free to change the docs/comments to your heart's content.

I'd like reserve a (protected?) wiki page, or something of the sort,
that we can point people to directly if they hit any grease failures.
"Server screwed up" is probably not enough context for a typical user
to know what to do next.

Seems sensible to have a place to explain something to authors. Why not
put it directly in the protocol docs though? (I'd be fine with a wiki
too, but a docs page is protected by definition)

I will also work on splitting 0002 into revertable and not-revertable
halves. The grease constant probably needs to remain documented and
reserved even if it doesn't do anything for 19.0.

Finally: is there any appetite for retaining the ability to grease
connections as production functionality, e.g. via
`max_protocol_version=grease`? Personally I think it'd be nice, but
it's not a trivial amount of extra work. We'd have to handle the case
where a future server responds with a legitimate minor version that's
newer than what our version of libpq supports. And I think we'd want a
production-grade version of this to add some randomization tricks, to
discourage people from keying on grease constants.

Both the patch split and max_protocol_version=grease sound reasonable to
me. I'd definitely like to keep all the grease code present on the main
branch, so we can keep using grease by default there.

I think max_protocol_version=grease makes a lot of sense. Because we
really want to make it as easy as possible for people to try out their
implementation of the negotation (see this for example[1]https://github.com/pgdogdev/pgdog/issues/578#issuecomment-3437244304)

If we do that then the patch split would be fairly minimal I expect.
i.e. it should only change the libpq default value, and the accompanying
test that tests the default value.

[1]: https://github.com/pgdogdev/pgdog/issues/578#issuecomment-3437244304

#13Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Jelte Fennema-Nio (#12)
Re: libpq: Bump protocol version to version 3.2 at least until the first/second beta

On Wed, Jan 14, 2026 at 2:16 PM Jelte Fennema-Nio <postgres@jeltef.nl> wrote:

All changes in those 3 additional patches look totally reasonable to me.

Thanks, I'll plan to squash those in v5, and probably kick 0005 out
into its own thread to give people a chance to object even if they're
ignoring the grease stuff.

I'd like reserve a (protected?) wiki page, or something of the sort,
that we can point people to directly if they hit any grease failures.
"Server screwed up" is probably not enough context for a typical user
to know what to do next.

Seems sensible to have a place to explain something to authors. Why not
put it directly in the protocol docs though? (I'd be fine with a wiki
too, but a docs page is protected by definition)

At the moment I can think of two reasons to put a "landing page" for
this in the wiki:

- Suggested improvements by users who land there can be made
immediately/cheaply/ephemerally, without either increasing the revert
burden mid-beta or making a committer feel that they have to wait to
get it "perfect" (because otherwise they flood the Postgres commit
graph with wiki-sized edits that are just going to be reverted
anyway). I think this grease phase will work best if we can be
maximally responsive to the people who take the time to talk to us.

- Informal, personal wiki voice (plus the ability to see a recent edit
date -- "yes, we're paying attention to you") seems like a better way
to encourage beta users to file bugs than formal project documentation
voice. YMMV on that.

Both the patch split and max_protocol_version=grease sound reasonable to
me. I'd definitely like to keep all the grease code present on the main
branch, so we can keep using grease by default there.

I think max_protocol_version=grease makes a lot of sense. Because we
really want to make it as easy as possible for people to try out their
implementation of the negotation (see this for example[1])

Yeah, I'd like to have that ability too. I don't know that I can
commit to writing or reviewing that amount of code for 19, though.
(And maybe there are lessons we'll learn during beta that can inform a
better production feature?)

--Jacob

#14Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Jacob Champion (#13)
Re: libpq: Bump protocol version to version 3.2 at least until the first/second beta

On Wed, Jan 14, 2026 at 2:56 PM Jacob Champion
<jacob.champion@enterprisedb.com> wrote:

I think max_protocol_version=grease makes a lot of sense. Because we
really want to make it as easy as possible for people to try out their
implementation of the negotation (see this for example[1])

Yeah, I'd like to have that ability too. I don't know that I can
commit to writing or reviewing that amount of code for 19, though.
(And maybe there are lessons we'll learn during beta that can inform a
better production feature?)

Per offline confusion/discussion: I plan to work on a grease feature
for beta _regardless_ of whether a "production-grade"
max_protocol_version=grease option turns out to be viable before
feature freeze; do not feel like you "have" to put work into the
latter just to get the former. Sorry about that.

--Jacob

#15Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Jacob Champion (#13)
4 attachment(s)
Re: libpq: Bump protocol version to version 3.2 at least until the first/second beta

On Wed, Jan 14, 2026 at 2:56 PM Jacob Champion
<jacob.champion@enterprisedb.com> wrote:

Thanks, I'll plan to squash those in v5, and probably kick 0005 out
into its own thread to give people a chance to object even if they're
ignoring the grease stuff.

0001, 0003, and 0005 are committed. v5 is attached with several
changes, described below.

On Wed, Jan 14, 2026 at 12:23 PM Jacob Champion
<jacob.champion@enterprisedb.com> wrote:

I want to more clearly decouple ourselves from TLS's GREASE in the
documentation and comments.

Done. Unfortunately the rewrites were too difficult to put into nice
squash! commits, since they ended up being spread across the split
described below, so I've also attached an "overall" diff file to try
to highlight what I changed from v3-0002.

One thing I tried to do here was separate the beta-only behavior into
<note>s, so that documentation writers can still review and patch the
language that's going to be published for release. I don't think that
will confuse the limited audience that is going to be reading this.

I will also work on splitting 0002 into revertable and not-revertable
halves. The grease constant probably needs to remain documented and
reserved even if it doesn't do anything for 19.0.

Done. My proposed split is in v5-0002 (which stays) and -0003 (which
gets reverted).

I also added an 0001 which (IMO) improves our documentation around
this, and adds a registry of sorts for the protocol extension
parameters. I'm not completely thrilled about the code and formatting
of that new registry table, but I think what I have is better than
nothing, so I'm going to stop fighting with docbook about this.

I'd like reserve a (protected?) wiki page, or something of the sort,
that we can point people to directly if they hit any grease failures.

This still needs to be done/discussed, but we have a good amount of time.

Finally: is there any appetite for retaining the ability to grease
connections as production functionality, e.g. via
`max_protocol_version=grease`?

This is on the back burner for now. (As stated upthread, it doesn't
need to block the beta-only behavior.)

WDYT?

Thanks,
--Jacob

Attachments:

since-v3.diff.txttext/plain; charset=US-ASCII; name=since-v3.diff.txtDownload
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index fb6ce177f0d..e08d46782cc 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2211,23 +2211,34 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
      <varlistentry id="libpq-connect-max-protocol-version" xreflabel="max_protocol_version">
       <term><literal>max_protocol_version</literal></term>
       <listitem>
-       <para>
-        Specifies the protocol version to request from the server.
-        During the PostgreSQL 19 beta period, the default is to use
-        <literal>3.9999</literal>, a GREASE (Generate Random Extensions And
-        Sustain Extensibility) value that tests proper protocol negotiation
-        implementation. If the server does not support the protocol version
-        requested by the client, the connection is automatically downgraded to
-        a lower minor protocol version that the server supports. After the
-        connection attempt has completed you can use
-        <xref linkend="libpq-PQfullProtocolVersion"/> to find out which exact
-        protocol version was negotiated.
-       </para>
+       <note>
+        <para>
+        During the PostgreSQL 19 beta period, libpq connections that do not
+        specify a <literal>max_protocol_version</literal> will "grease" the
+        handshake by sending unsupported startup parameters, including version
+        <literal>3.9999</literal>, in order to identify software that does not
+        correctly negotiate the connection. This replaces the default behavior
+        described below.
+        </para>
+        <para>
+        If you know that a server doesn't properly implement protocol version
+        negotiation, you can set <literal>max_protocol_version=3.0</literal> to
+        revert to the standard behavior (preferably after notifying the server's
+        maintainers that their software needs to be fixed).
+        </para>
+       </note>
 
        <para>
-        For servers that don't properly implement protocol version negotiation,
-        you can set <literal>max_protocol_version=3.0</literal> to connect
-        successfully.
+        Specifies the protocol version to request from the server.
+        The default is to use version <literal>3.0</literal> of the
+        <productname>PostgreSQL</productname> protocol, unless the connection
+        string specifies a feature that relies on a higher protocol version,
+        in which case the latest version supported by libpq is used. If the
+        server does not support the protocol version requested by the client,
+        the connection is automatically downgraded to a lower minor protocol
+        version that the server supports. After the connection attempt has
+        completed you can use <xref linkend="libpq-PQfullProtocolVersion"/> to
+        find out which exact protocol version was negotiated.
        </para>
 
        <para>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index c61dd2bf948..45148a71d63 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -189,18 +189,26 @@
   </sect2>
 
   <sect2 id="protocol-versions">
-   <title>Protocol Versions</title>
+   <title>Protocol Versions and Extensibility</title>
 
    <para>
-    The current, latest version of the protocol is version 3.2. During the
-    PostgreSQL 19 beta period, libpq defaults to requesting protocol version
-    3.9999 to test that servers and middleware properly implement protocol
-    version negotiation. Servers that support negotiation will automatically
-    downgrade to version 3.2 or 3.0. For servers that don't support
-    negotiation, users can connect by explicitly setting
-    <literal>max_protocol_version=3.0</literal> in their connection string.
+    The current, latest version of the protocol is version 3.2. However, for
+    backwards compatibility with old server versions and middleware that don't
+    support the version negotiation yet, libpq still uses protocol version 3.0
+    by default.
    </para>
 
+   <note>
+     <para>
+      During the PostgreSQL 19 beta period, libpq will instead default to
+      requesting protocol version 3.9999, to test that servers and middleware
+      properly implement protocol version negotiation. Servers that support
+      negotiation will automatically downgrade to version 3.2 or 3.0. Users can
+      bypass this beta-only behavior by explicitly setting
+      <literal>max_protocol_version=3.0</literal> in their connection string.
+     </para>
+   </note>
+
    <para>
     A single server can support multiple protocol versions.  The initial
     startup-request message tells the server which protocol version the client
@@ -226,10 +234,12 @@
    <para>
     <xref linkend="protocol-versions-table"/> shows the currently supported
     protocol versions.
+    <xref linkend="other-protocol-versions-table"/>
+    documents protocol versions that are unsupported or otherwise reserved.
    </para>
 
    <table id="protocol-versions-table">
-    <title>Protocol Versions</title>
+    <title>Supported Protocol Versions</title>
 
     <tgroup cols="3">
      <thead>
@@ -241,20 +251,6 @@
      </thead>
 
      <tbody>
-      <row>
-      <entry>3.9999</entry>
-      <entry>-</entry>
-      <entry>GREASE (Generate Random Extensions And Sustain Extensibility)
-        version. This version number is intentionally reserved and will never
-        be implemented. During the PostgreSQL 19 beta period, libpq requests
-        this version by default to test that servers and middleware properly
-        implement protocol version negotiation via
-        <literal>NegotiateProtocolVersion</literal>. Servers should respond
-        by downgrading to a supported version. This mechanism helps ensure
-        the ecosystem is ready for future protocol versions. libpq will revert
-        to defaulting to version 3.2 before the PostgreSQL 19 final release.
-      </entry>
-      </row>
       <row>
       <entry>3.2</entry>
       <entry>PostgreSQL 18 and later</entry>
@@ -265,6 +261,39 @@
       </entry>
       </row>
       <row>
+      <entry>3.0</entry>
+      <entry>PostgreSQL 7.4 and later</entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+   <table id="other-protocol-versions-table">
+    <title>Other Protocol Versions</title>
+
+    <tgroup cols="3">
+     <thead>
+      <row>
+       <entry>Version</entry>
+       <entry>Supported by</entry>
+       <entry>Description</entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+      <entry>3.9999</entry>
+      <entry>-</entry>
+      <entry>Reserved for protocol greasing. libpq may use this version, which
+        is higher than any minor version the project ever expects to use, to
+        test that servers and middleware properly implement protocol version
+        negotiation. Servers <emphasis>must not</emphasis> add special-case
+        logic for this version; they should simply compare it to their latest
+        supported version (which will always be smaller) and downgrade via a
+        NegotiateProtocolVersion message.
+      </entry>
+      </row>
+      <row>
       <entry>3.1</entry>
       <entry>-</entry>
       <entry>Reserved. Version 3.1 has not been used by any PostgreSQL
@@ -274,15 +303,93 @@
       </entry>
       </row>
       <row>
-      <entry>3.0</entry>
-      <entry>PostgreSQL 7.4 and later</entry>
-      </row>
-      <row>
       <entry>2.0</entry>
       <entry>up to PostgreSQL 13</entry>
-      <entry>See previous releases of
+      <entry>Obsolete. See previous releases of
       the <productname>PostgreSQL</productname> documentation for
-      details</entry>
+      details.</entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+   <para id="protocol-extensions">
+    Servers and clients may additionally negotiate individual extensions to the
+    protocol version in use. These are offered by the client as specially-named
+    parameters in the startup message. Servers reject any unknown or unsupported
+    extensions by sending a NegotiateProtocolVersion message containing the list
+    of rejected parameter names, at which point the client may choose whether to
+    continue with the connection. <xref linkend="protocol-extensions-table"/>
+    shows the current list of protocol extension parameters.
+   </para>
+
+   <table id="protocol-extensions-table" rowheader="firstcol">
+    <title>Protocol Extensions</title>
+
+    <tgroup cols="5">
+     <!-- For spanning, below. -->
+     <colspec colname="first" colsep="0"/>
+     <colspec colname="name" colnum="2"/>
+     <colspec colname="vals" colnum="3"/>
+     <colspec colname="support" colnum="4"/>
+     <colspec colname="last" colnum="5"/>
+
+     <!--
+       "all" spans the width of the table; it is used for the section headers.
+       "reserved" spans the Values and Supported columns, for the Reserved
+       names which define neither.
+     -->
+     <spanspec spanname="all" namest="first" nameend="last"/>
+     <spanspec spanname="reserved" namest="vals" nameend="support"/>
+
+     <thead>
+      <row>
+       <!--
+         Row header. Use namest="name" to skip to the next column.
+         XXX this causes a thin "indent" effect for the other rows
+       -->
+       <entry/>
+
+       <entry>Parameter&nbsp;Name</entry>
+       <entry>Values</entry>
+       <entry>Supported&nbsp;by</entry>
+       <entry>Description</entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row><entry spanname="all">Defined</entry></row>
+
+      <row>
+       <entry namest="last" align="center" valign="middle">
+        <emphasis>(No supported protocol extensions are currently defined.)</emphasis>
+       </entry>
+      </row>
+
+      <row><entry spanname="all">Reserved</entry></row>
+
+      <row>
+      <entry namest="name"><literal>_pq_.<replaceable>[name]</replaceable></literal></entry>
+      <entry spanname="reserved">-</entry>
+      <entry>Any other parameter names beginning with <literal>_pq_.</literal>,
+        that are not defined above, are reserved for future protocol expansion.
+        Servers <emphasis>must</emphasis> reject any that are received from a
+        client, by sending a NegotiateProtocolVersion message during the
+        <link linkend="protocol-flow-start-up">startup flow</link>, and should
+        otherwise continue the connection.
+      </entry>
+      </row>
+
+      <row>
+      <entry namest="name"><literal>_pq_.test_protocol_negotiation</literal></entry>
+      <entry spanname="reserved">-</entry>
+      <entry>Reserved for protocol greasing. libpq may send this extension to
+        test that servers and middleware properly implement protocol extension
+        negotiation. Servers <emphasis>must not</emphasis> add special-case
+        logic for this parameter; they should simply send the list of all
+        unsupported options (including this one) via a NegotiateProtocolVersion
+        message.
+      </entry>
       </row>
      </tbody>
     </tgroup>
@@ -312,8 +419,8 @@
     To begin a session, a frontend opens a connection to the server and sends
     a startup message.  This message includes the names of the user and of the
     database the user wants to connect to; it also identifies the particular
-    protocol version to be used.  (Optionally, the startup message can include
-    additional settings for run-time parameters.)
+    protocol version to be used.  (Optionally, the startup message can request
+    protocol extensions and include additional settings for run-time parameters.)
     The server then uses this information and
     the contents of its configuration files (such as
     <filename>pg_hba.conf</filename>) to determine
@@ -6164,29 +6271,13 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
             </para>
            </listitem>
           </varlistentry>
-          <varlistentry>
-           <term><literal>_pq_.test_protocol_negotiation</literal></term>
-           <listitem>
-            <para>
-             A reserved protocol extension requested by libpq during the
-             PostgreSQL 19 beta period to test that servers properly implement
-             protocol version negotiation. When the client requests the GREASE
-             protocol version (3.9999), this parameter is automatically
-             included in the startup packet too. Servers should report it as
-             unsupported in their <literal>NegotiateProtocolVersion</literal>
-             response. In GREASE mode the connection will fail if the server
-             doesn't report this parameter as unsupported, ensuring
-             comprehensive implementation of protocol negotiation. This
-             parameter is reserved and will never actually be implemented by a
-             server.
-            </para>
-           </listitem>
-          </varlistentry>
          </variablelist>
 
          In addition to the above, other parameters may be listed.
          Parameter names beginning with <literal>_pq_.</literal> are
-         reserved for use as protocol extensions, while others are
+         reserved for use as
+         <link linkend="protocol-extensions">protocol extensions</link>,
+         while others are
          treated as run-time parameters to be set at backend start
          time.  Such settings will be applied during backend start
          (after parsing the command-line arguments if any) and will
diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index 0c3583ce4d9..a29c9c94d79 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -90,16 +90,9 @@ is_unixsock_path(const char *path)
 
 /*
  * The earliest and latest frontend/backend protocol version supported.
- *
- * PG_PROTOCOL_GREASE is an intentionally unsupported protocol version used
- * for GREASE (Generate Random Extensions And Sustain Extensibility). This
- * helps ensure that servers properly implement protocol version negotiation
- * via NegotiateProtocolVersion. Version 3.9999 was chosen to be safely within
- * the valid range but unlikely to ever be implemented.
  */
 #define PG_PROTOCOL_EARLIEST	PG_PROTOCOL(3,0)
 #define PG_PROTOCOL_LATEST		PG_PROTOCOL(3,2)
-#define PG_PROTOCOL_GREASE		PG_PROTOCOL(3,9999)
 
 /*
  * Reserved protocol numbers, which have special semantics:
@@ -111,6 +104,16 @@ is_unixsock_path(const char *path)
  */
 #define PG_PROTOCOL_RESERVED_31		PG_PROTOCOL(3,1)
 
+/*
+ * PG_PROTOCOL_GREASE is an intentionally unsupported protocol version used
+ * for "greasing" (the practice of sending valid, but extraneous or otherwise
+ * unusual, messages to keep peer implementations honest). This helps ensure
+ * that servers properly implement protocol version negotiation. Version 3.9999
+ * was chosen since it is safely within the valid range, it is representable
+ * via PQfullProtocolVersion, and it is unlikely to ever be needed in practice.
+ */
+#define PG_PROTOCOL_GREASE		PG_PROTOCOL(3,9999)
+
 /*
  * A client can send a cancel-current-operation request to the postmaster.
  * This is uglier than sending it directly to the client's backend, but it
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 9fcf094a36f..c42f38cbc99 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -2142,10 +2142,11 @@ pqConnectOptions2(PGconn *conn)
 	else
 	{
 		/*
-		 * Default to the GREASE protocol version to test that servers
-		 * properly implement NegotiateProtocolVersion. The server will
-		 * automatically downgrade to a supported version. This will be
-		 * changed to a supported version before the PG19 release.
+		 * Default to PG_PROTOCOL_GREASE, which is larger than all real
+		 * versions, to test negotiation. The server should automatically
+		 * downgrade to a supported version.
+		 *
+		 * This behavior is for 19beta only. It will be reverted before RC1.
 		 */
 		conn->max_pversion = PG_PROTOCOL_GREASE;
 	}
@@ -4386,7 +4387,7 @@ keep_going:						/* We will come back to here until there is
 				if (conn->max_pversion == PG_PROTOCOL_GREASE &&
 					conn->pversion == PG_PROTOCOL_GREASE)
 				{
-					libpq_append_conn_error(conn, "server incorrectly accepted reserved GREASE protocol version 3.9999 without negotiation");
+					libpq_append_conn_error(conn, "server incorrectly accepted \"grease\" protocol version 3.9999 without negotiation");
 					goto error_return;
 				}
 
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 4f78b88b3a8..fc011e89450 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -1453,7 +1453,19 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 	if (pqGetInt(&num, 4, conn) != 0)
 		goto eof;
 
-	/* Check the protocol version */
+	/*
+	 * Check the protocol version.
+	 *
+	 * PG_PROTOCOL_GREASE is intentionally unsupported and reserved. It's
+	 * higher than any real version, so check for that first, to get the most
+	 * specific error message. Then check the upper and lower bounds.
+	 */
+	if (their_version == PG_PROTOCOL_GREASE)
+	{
+		libpq_append_conn_error(conn, "received invalid protocol negotiation message: server requested \"grease\" protocol version 3.9999");
+		goto failure;
+	}
+
 	if (their_version > conn->pversion)
 	{
 		libpq_append_conn_error(conn, "received invalid protocol negotiation message: server requested downgrade to a higher-numbered version");
@@ -1473,13 +1485,6 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 		goto failure;
 	}
 
-	/* The GREASE protocol version is intentionally unsupported and reserved */
-	if (their_version == PG_PROTOCOL_GREASE)
-	{
-		libpq_append_conn_error(conn, "received invalid protocol negotiation message: server claimed to support reserved GREASE protocol version 3.9999");
-		goto failure;
-	}
-
 	if (num < 0)
 	{
 		libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported negative number of unsupported parameters");
@@ -1527,19 +1532,21 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 		}
 
 		/* Check if this is the expected test parameter */
-		if (expect_test_protocol_negotiation && strcmp(conn->workBuffer.data, "_pq_.test_protocol_negotiation") == 0)
+		if (expect_test_protocol_negotiation &&
+			strcmp(conn->workBuffer.data, "_pq_.test_protocol_negotiation") == 0)
 		{
 			found_test_protocol_negotiation = true;
 		}
 		else
 		{
-			libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported an unsupported parameter that was not requested (\"%s\")", conn->workBuffer.data);
+			libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported an unsupported parameter that was not requested (\"%s\")",
+									conn->workBuffer.data);
 			goto failure;
 		}
 	}
 
 	/*
-	 * If we requested the GREASE protocol version, the server must report
+	 * If we requested protocol grease, the server must report
 	 * _pq_.test_protocol_negotiation as unsupported. This ensures
 	 * comprehensive NegotiateProtocolVersion implementation.
 	 */
@@ -2497,9 +2504,9 @@ build_startup_packet(const PGconn *conn, char *packet,
 		ADD_STARTUP_OPTION("client_encoding", conn->client_encoding_initial);
 
 	/*
-	 * Add the test protocol negotiation option if we're using the GREASE
-	 * protocol version. This tests that servers properly report unsupported
-	 * protocol options in their NegotiateProtocolVersion response.
+	 * Add the test_protocol_negotiation option when greasing, to test that
+	 * servers properly report unsupported protocol options in addition to
+	 * unsupported minor versions.
 	 */
 	if (conn->pversion == PG_PROTOCOL_GREASE)
 		ADD_STARTUP_OPTION("_pq_.test_protocol_negotiation", "");
v5-0001-doc-Expand-upon-protocol-versions-and-extensions.patchapplication/octet-stream; name=v5-0001-doc-Expand-upon-protocol-versions-and-extensions.patchDownload
From 404247348f6328aea4206cf5252ee9dbd9794f84 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Wed, 21 Jan 2026 14:31:47 -0800
Subject: [PATCH v5 1/3] doc: Expand upon protocol versions and extensions

First, split the Protocol Versions table in two, and lead with the list
of versions that are supported today. Reserved and unsupported version
numbers go into the second table.

Second, in anticipation of a new (reserved) protocol extension, document
the extension negotiation process alongside version negotiation, and add
a table to contain extension parameter registrations in the future. (I
considered splitting this in two as well, but then the first table would
be empty for a long while, which seemed silly.)

Discussion: https://postgr.es/m/DDPR5BPWH1RJ.1LWAK6QAURVAY%40jeltef.nl
---
 doc/src/sgml/protocol.sgml | 113 +++++++++++++++++++++++++++++++++----
 1 file changed, 102 insertions(+), 11 deletions(-)

diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index a2b528c481e..792ae73b369 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -189,7 +189,7 @@
   </sect2>
 
   <sect2 id="protocol-versions">
-   <title>Protocol Versions</title>
+   <title>Protocol Versions and Extensibility</title>
 
    <para>
     The current, latest version of the protocol is version 3.2. However, for
@@ -223,10 +223,12 @@
    <para>
     <xref linkend="protocol-versions-table"/> shows the currently supported
     protocol versions.
+    <xref linkend="other-protocol-versions-table"/>
+    documents protocol versions that are unsupported or otherwise reserved.
    </para>
 
    <table id="protocol-versions-table">
-    <title>Protocol Versions</title>
+    <title>Supported Protocol Versions</title>
 
     <tgroup cols="3">
      <thead>
@@ -247,6 +249,27 @@
         message was redefined to have a variable length payload.
       </entry>
       </row>
+      <row>
+      <entry>3.0</entry>
+      <entry>PostgreSQL 7.4 and later</entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+   <table id="other-protocol-versions-table">
+    <title>Other Protocol Versions</title>
+
+    <tgroup cols="3">
+     <thead>
+      <row>
+       <entry>Version</entry>
+       <entry>Supported by</entry>
+       <entry>Description</entry>
+      </row>
+     </thead>
+
+     <tbody>
       <row>
       <entry>3.1</entry>
       <entry>-</entry>
@@ -257,15 +280,81 @@
       </entry>
       </row>
       <row>
-      <entry>3.0</entry>
-      <entry>PostgreSQL 7.4 and later</entry>
-      </row>
-      <row>
       <entry>2.0</entry>
       <entry>up to PostgreSQL 13</entry>
-      <entry>See previous releases of
+      <entry>Obsolete. See previous releases of
       the <productname>PostgreSQL</productname> documentation for
-      details</entry>
+      details.</entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
+
+   <para id="protocol-extensions">
+    Servers and clients may additionally negotiate individual extensions to the
+    protocol version in use. These are offered by the client as specially-named
+    parameters in the startup message. Servers reject any unknown or unsupported
+    extensions by sending a NegotiateProtocolVersion message containing the list
+    of rejected parameter names, at which point the client may choose whether to
+    continue with the connection. <xref linkend="protocol-extensions-table"/>
+    shows the current list of protocol extension parameters.
+   </para>
+
+   <table id="protocol-extensions-table" rowheader="firstcol">
+    <title>Protocol Extensions</title>
+
+    <tgroup cols="5">
+     <!-- For spanning, below. -->
+     <colspec colname="first" colsep="0"/>
+     <colspec colname="name" colnum="2"/>
+     <colspec colname="vals" colnum="3"/>
+     <colspec colname="support" colnum="4"/>
+     <colspec colname="last" colnum="5"/>
+
+     <!--
+       "all" spans the width of the table; it is used for the section headers.
+       "reserved" spans the Values and Supported columns, for the Reserved
+       names which define neither.
+     -->
+     <spanspec spanname="all" namest="first" nameend="last"/>
+     <spanspec spanname="reserved" namest="vals" nameend="support"/>
+
+     <thead>
+      <row>
+       <!--
+         Row header. Use namest="name" to skip to the next column.
+         XXX this causes a thin "indent" effect for the other rows
+       -->
+       <entry/>
+
+       <entry>Parameter&nbsp;Name</entry>
+       <entry>Values</entry>
+       <entry>Supported&nbsp;by</entry>
+       <entry>Description</entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row><entry spanname="all">Defined</entry></row>
+
+      <row>
+       <entry namest="last" align="center" valign="middle">
+        <emphasis>(No supported protocol extensions are currently defined.)</emphasis>
+       </entry>
+      </row>
+
+      <row><entry spanname="all">Reserved</entry></row>
+
+      <row>
+      <entry namest="name"><literal>_pq_.<replaceable>[name]</replaceable></literal></entry>
+      <entry spanname="reserved">-</entry>
+      <entry>Any other parameter names beginning with <literal>_pq_.</literal>,
+        that are not defined above, are reserved for future protocol expansion.
+        Servers <emphasis>must</emphasis> reject any that are received from a
+        client, by sending a NegotiateProtocolVersion message during the
+        <link linkend="protocol-flow-start-up">startup flow</link>, and should
+        otherwise continue the connection.
+      </entry>
       </row>
      </tbody>
     </tgroup>
@@ -295,8 +384,8 @@
     To begin a session, a frontend opens a connection to the server and sends
     a startup message.  This message includes the names of the user and of the
     database the user wants to connect to; it also identifies the particular
-    protocol version to be used.  (Optionally, the startup message can include
-    additional settings for run-time parameters.)
+    protocol version to be used.  (Optionally, the startup message can request
+    protocol extensions and include additional settings for run-time parameters.)
     The server then uses this information and
     the contents of its configuration files (such as
     <filename>pg_hba.conf</filename>) to determine
@@ -6151,7 +6240,9 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
 
          In addition to the above, other parameters may be listed.
          Parameter names beginning with <literal>_pq_.</literal> are
-         reserved for use as protocol extensions, while others are
+         reserved for use as
+         <link linkend="protocol-extensions">protocol extensions</link>,
+         while others are
          treated as run-time parameters to be set at backend start
          time.  Such settings will be applied during backend start
          (after parsing the command-line arguments if any) and will
-- 
2.34.1

v5-0002-libpq-Prepare-for-protocol-grease-during-19beta.patchapplication/octet-stream; name=v5-0002-libpq-Prepare-for-protocol-grease-during-19beta.patchDownload
From 11e57bed6f6b7037ebc83d2c0da2afea89d6c932 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Thu, 22 Jan 2026 14:45:57 -0800
Subject: [PATCH v5 2/3] libpq: Prepare for protocol grease during 19beta

The main reason that libpq doesn't request protocol version 3.2 by
default is because other proxy/server implementations don't implement
the negotiation. This is a bit of a chicken-and-egg problem: We don't
bump the default version that libpq requests, but other implementations
may not be incentivized to implement version negotiation if their users
never run into issues.

One established practice to combat this is to flip Postel's Law on its
head, by sending parameters that the server cannot possibly support. If
the server fails the handshake instead of correctly negotiating, then
the problem is surfaced naturally. If the server instead claims to
support the bogus parameters, then we fail the connection to make the
lie obvious. This is called "grease" (or "greasing"), after the GREASE
mechanism in TLS that popularized the concept:

    https://www.rfc-editor.org/rfc/rfc8701.html

This patch reserves 3.9999 as an explicitly unsupported protocol version
number and `_pq_.test_protocol_negotiation` as an explicitly unsupported
protocol extension. A later commit will send these by default in order
to stress-test the ecosystem during the beta period; that commit will
then be reverted before 19 RC1, so that we can decide what to do with
whatever data has been gathered.

The _pq_.test_protocol_negotiation change here is intentionally docs-
only: after its implementation is reverted, the parameter should remain
reserved.

Extracted/adapted from a patch by Jelte Fennema-Nio.

Author: Jelte Fennema-Nio <postgres@jeltef.nl>
Co-authored-by: Jacob Champion <jacob.champion@enterprisedb.com>
Discussion: https://postgr.es/m/DDPR5BPWH1RJ.1LWAK6QAURVAY%40jeltef.nl
---
 doc/src/sgml/protocol.sgml          | 24 ++++++++++++++++++++++++
 src/include/libpq/pqcomm.h          | 10 ++++++++++
 src/interfaces/libpq/fe-protocol3.c | 14 +++++++++++++-
 3 files changed, 47 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 792ae73b369..cefd6f33f9b 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -270,6 +270,18 @@
      </thead>
 
      <tbody>
+      <row>
+      <entry>3.9999</entry>
+      <entry>-</entry>
+      <entry>Reserved for protocol greasing. libpq may use this version, which
+        is higher than any minor version the project ever expects to use, to
+        test that servers and middleware properly implement protocol version
+        negotiation. Servers <emphasis>must not</emphasis> add special-case
+        logic for this version; they should simply compare it to their latest
+        supported version (which will always be smaller) and downgrade via a
+        NegotiateProtocolVersion message.
+      </entry>
+      </row>
       <row>
       <entry>3.1</entry>
       <entry>-</entry>
@@ -356,6 +368,18 @@
         otherwise continue the connection.
       </entry>
       </row>
+
+      <row>
+      <entry namest="name"><literal>_pq_.test_protocol_negotiation</literal></entry>
+      <entry spanname="reserved">-</entry>
+      <entry>Reserved for protocol greasing. libpq may send this extension to
+        test that servers and middleware properly implement protocol extension
+        negotiation. Servers <emphasis>must not</emphasis> add special-case
+        logic for this parameter; they should simply send the list of all
+        unsupported options (including this one) via a NegotiateProtocolVersion
+        message.
+      </entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index 1bbe5b9ee45..a29c9c94d79 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -104,6 +104,16 @@ is_unixsock_path(const char *path)
  */
 #define PG_PROTOCOL_RESERVED_31		PG_PROTOCOL(3,1)
 
+/*
+ * PG_PROTOCOL_GREASE is an intentionally unsupported protocol version used
+ * for "greasing" (the practice of sending valid, but extraneous or otherwise
+ * unusual, messages to keep peer implementations honest). This helps ensure
+ * that servers properly implement protocol version negotiation. Version 3.9999
+ * was chosen since it is safely within the valid range, it is representable
+ * via PQfullProtocolVersion, and it is unlikely to ever be needed in practice.
+ */
+#define PG_PROTOCOL_GREASE		PG_PROTOCOL(3,9999)
+
 /*
  * A client can send a cancel-current-operation request to the postmaster.
  * This is uglier than sending it directly to the client's backend, but it
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 103428033ef..90bbb2eba1f 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -1451,7 +1451,19 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 	if (pqGetInt(&num, 4, conn) != 0)
 		goto eof;
 
-	/* Check the protocol version */
+	/*
+	 * Check the protocol version.
+	 *
+	 * PG_PROTOCOL_GREASE is intentionally unsupported and reserved. It's
+	 * higher than any real version, so check for that first, to get the most
+	 * specific error message. Then check the upper and lower bounds.
+	 */
+	if (their_version == PG_PROTOCOL_GREASE)
+	{
+		libpq_append_conn_error(conn, "received invalid protocol negotiation message: server requested \"grease\" protocol version 3.9999");
+		goto failure;
+	}
+
 	if (their_version > conn->pversion)
 	{
 		libpq_append_conn_error(conn, "received invalid protocol negotiation message: server requested downgrade to a higher-numbered version");
-- 
2.34.1

v5-0003-libpq-Grease-the-protocol-by-default.patchapplication/octet-stream; name=v5-0003-libpq-Grease-the-protocol-by-default.patchDownload
From 9af87739be8403dda322f8857362c2e4b42a01c8 Mon Sep 17 00:00:00 2001
From: Jacob Champion <jacob.champion@enterprisedb.com>
Date: Thu, 22 Jan 2026 16:09:03 -0800
Subject: [PATCH v5 3/3] libpq: Grease the protocol by default

Send PG_PROTOCOL_GREASE and _pq_.test_protocol_negotiation, which were
introduced in commit TODO, by default, and fail the connection if the
server attempts to claim support for them. The hope is to provide
feedback to noncompliant implementations and gain confidence in our
ability to advance the protocol. (See the other commit for details.)

It's still possible for users to connect to servers that don't support
protocol negotiation, by using max_protocol_version=3.0 in their
connection strings. Only the default connection behavior is impacted.

This commit is tracked as a PG19 open item and will be reverted before
RC1. (The implementation here doesn't handle negotiation with later
server versions, so it can't be released into the wild as a
five-year-supported feature. But an improved implementation might be
able to do so, in the future...)

Author: Jelte Fennema-Nio <postgres@jeltef.nl>
Co-authored-by: Jacob Champion <jacob.champion@enterprisedb.com>
Discussion: https://postgr.es/m/DDPR5BPWH1RJ.1LWAK6QAURVAY%40jeltef.nl
---
 doc/src/sgml/libpq.sgml                       | 17 ++++++++
 doc/src/sgml/protocol.sgml                    | 11 +++++
 src/interfaces/libpq/fe-connect.c             | 21 ++++++----
 src/interfaces/libpq/fe-protocol3.c           | 41 +++++++++++++++++--
 .../modules/libpq_pipeline/libpq_pipeline.c   |  6 +--
 5 files changed, 82 insertions(+), 14 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 21e1ba34a4e..e08d46782cc 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2211,6 +2211,23 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
      <varlistentry id="libpq-connect-max-protocol-version" xreflabel="max_protocol_version">
       <term><literal>max_protocol_version</literal></term>
       <listitem>
+       <note>
+        <para>
+        During the PostgreSQL 19 beta period, libpq connections that do not
+        specify a <literal>max_protocol_version</literal> will "grease" the
+        handshake by sending unsupported startup parameters, including version
+        <literal>3.9999</literal>, in order to identify software that does not
+        correctly negotiate the connection. This replaces the default behavior
+        described below.
+        </para>
+        <para>
+        If you know that a server doesn't properly implement protocol version
+        negotiation, you can set <literal>max_protocol_version=3.0</literal> to
+        revert to the standard behavior (preferably after notifying the server's
+        maintainers that their software needs to be fixed).
+        </para>
+       </note>
+
        <para>
         Specifies the protocol version to request from the server.
         The default is to use version <literal>3.0</literal> of the
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index cefd6f33f9b..45148a71d63 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -198,6 +198,17 @@
     by default.
    </para>
 
+   <note>
+     <para>
+      During the PostgreSQL 19 beta period, libpq will instead default to
+      requesting protocol version 3.9999, to test that servers and middleware
+      properly implement protocol version negotiation. Servers that support
+      negotiation will automatically downgrade to version 3.2 or 3.0. Users can
+      bypass this beta-only behavior by explicitly setting
+      <literal>max_protocol_version=3.0</literal> in their connection string.
+     </para>
+   </note>
+
    <para>
     A single server can support multiple protocol versions.  The initial
     startup-request message tells the server which protocol version the client
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index a0d2f749811..c42f38cbc99 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -2142,15 +2142,13 @@ pqConnectOptions2(PGconn *conn)
 	else
 	{
 		/*
-		 * To not break connecting to older servers/poolers that do not yet
-		 * support NegotiateProtocolVersion, default to the 3.0 protocol at
-		 * least for a while longer. Except when min_protocol_version is set
-		 * to something larger, then we might as well default to the latest.
+		 * Default to PG_PROTOCOL_GREASE, which is larger than all real
+		 * versions, to test negotiation. The server should automatically
+		 * downgrade to a supported version.
+		 *
+		 * This behavior is for 19beta only. It will be reverted before RC1.
 		 */
-		if (conn->min_pversion > PG_PROTOCOL(3, 0))
-			conn->max_pversion = PG_PROTOCOL_LATEST;
-		else
-			conn->max_pversion = PG_PROTOCOL(3, 0);
+		conn->max_pversion = PG_PROTOCOL_GREASE;
 	}
 
 	if (conn->min_pversion > conn->max_pversion)
@@ -4386,6 +4384,13 @@ keep_going:						/* We will come back to here until there is
 					goto error_return;
 				}
 
+				if (conn->max_pversion == PG_PROTOCOL_GREASE &&
+					conn->pversion == PG_PROTOCOL_GREASE)
+				{
+					libpq_append_conn_error(conn, "server incorrectly accepted \"grease\" protocol version 3.9999 without negotiation");
+					goto error_return;
+				}
+
 				/* Almost there now ... */
 				conn->status = CONNECTION_CHECK_TARGET;
 				goto keep_going;
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 90bbb2eba1f..fc011e89450 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -1444,6 +1444,8 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 {
 	int			their_version;
 	int			num;
+	bool		found_test_protocol_negotiation;
+	bool		expect_test_protocol_negotiation;
 
 	if (pqGetInt(&their_version, 4, conn) != 0)
 		goto eof;
@@ -1511,9 +1513,12 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 	conn->pversion = their_version;
 
 	/*
-	 * We don't currently request any protocol extensions, so we don't expect
-	 * the server to reply with any either.
+	 * Check that all expected unsupported parameters are reported by the
+	 * server.
 	 */
+	found_test_protocol_negotiation = false;
+	expect_test_protocol_negotiation = (conn->max_pversion == PG_PROTOCOL_GREASE);
+
 	for (int i = 0; i < num; i++)
 	{
 		if (pqGets(&conn->workBuffer, conn))
@@ -1525,7 +1530,29 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 			libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported unsupported parameter name without a \"%s\" prefix (\"%s\")", "_pq_.", conn->workBuffer.data);
 			goto failure;
 		}
-		libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported an unsupported parameter that was not requested (\"%s\")", conn->workBuffer.data);
+
+		/* Check if this is the expected test parameter */
+		if (expect_test_protocol_negotiation &&
+			strcmp(conn->workBuffer.data, "_pq_.test_protocol_negotiation") == 0)
+		{
+			found_test_protocol_negotiation = true;
+		}
+		else
+		{
+			libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported an unsupported parameter that was not requested (\"%s\")",
+									conn->workBuffer.data);
+			goto failure;
+		}
+	}
+
+	/*
+	 * If we requested protocol grease, the server must report
+	 * _pq_.test_protocol_negotiation as unsupported. This ensures
+	 * comprehensive NegotiateProtocolVersion implementation.
+	 */
+	if (expect_test_protocol_negotiation && !found_test_protocol_negotiation)
+	{
+		libpq_append_conn_error(conn, "server did not report the unsupported `_pq_.test_protocol_negotiation` parameter in its protocol negotiation message");
 		goto failure;
 	}
 
@@ -2476,6 +2503,14 @@ build_startup_packet(const PGconn *conn, char *packet,
 	if (conn->client_encoding_initial && conn->client_encoding_initial[0])
 		ADD_STARTUP_OPTION("client_encoding", conn->client_encoding_initial);
 
+	/*
+	 * Add the test_protocol_negotiation option when greasing, to test that
+	 * servers properly report unsupported protocol options in addition to
+	 * unsupported minor versions.
+	 */
+	if (conn->pversion == PG_PROTOCOL_GREASE)
+		ADD_STARTUP_OPTION("_pq_.test_protocol_negotiation", "");
+
 	/* Add any environment-driven GUC settings needed */
 	for (next_eo = options; next_eo->envName; next_eo++)
 	{
diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c
index 0fb44be32ce..b819bcc273c 100644
--- a/src/test/modules/libpq_pipeline/libpq_pipeline.c
+++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c
@@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn)
 	Assert(max_protocol_version_index >= 0);
 
 	/*
-	 * Test default protocol_version
+	 * Test default protocol_version (GREASE - should negotiate down to 3.2)
 	 */
 	vals[max_protocol_version_index] = "";
 	conn = PQconnectdbParams(keywords, vals, false);
@@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn)
 				 PQerrorMessage(conn));
 
 	protocol_version = PQfullProtocolVersion(conn);
-	if (protocol_version != 30000)
-		pg_fatal("expected 30000, got %d", protocol_version);
+	if (protocol_version != 30002)
+		pg_fatal("expected 30002, got %d", protocol_version);
 
 	PQfinish(conn);
 
-- 
2.34.1

#16Jelte Fennema-Nio
postgres@jeltef.nl
In reply to: Jacob Champion (#14)
5 attachment(s)
Re: libpq: Bump protocol version to version 3.2 at least until the first/second beta

On Thu Jan 15, 2026 at 1:31 AM CET, Jacob Champion wrote:

Per offline confusion/discussion: I plan to work on a grease feature
for beta _regardless_ of whether a "production-grade"
max_protocol_version=grease option turns out to be viable before
feature freeze;

I saw that you committed a few patches. Given that you said you wanted
to wordsmith the last one, this seemed like a good time to send in some
of my own improvements in that regard. (I did not change the GREASE
mentions, since you clearly had something in mind for those).

0001 is mine an your squash commit, squashed together and rebased on top of main.

0002 is the improvements to the docs. The one important thing is a
change from 3.2 to 3.0. Other than that it introduces a table for
tracking protocol extensions. That way other patches (like GoAway)
that introduce a protocol extension already have some location in
the docs where it can be listed.

0003-0005 are an attempt at making a bit more of a robust GREASE. 0003
makes it "harder to still implement negotation incorrectly". Then 0004
makes it not a hard failure if you connect with
max_protocol_version=grease to a new server. Then 0005 adds some
preliminary docs. These almost certainly need some more discussion. And
I don't expect them to necessarily get in for PG19, but if you like some
of it feel free to pick and take what you want from them. e.g. the
randomized protocol extensions are kinda nice IMO. But I'm not exactly
sure if the randomized version number is that much more useful though
than a fixed one.

Attachments:

v5-0001-libpq-Request-protocol-version-3.9999-to-GREASE-t.patchtext/x-patch; charset=utf-8; name=v5-0001-libpq-Request-protocol-version-3.9999-to-GREASE-t.patchDownload
From aeb772c789170e17c5c753445a8e0c06340dca81 Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Thu, 23 Oct 2025 15:08:52 +0200
Subject: [PATCH v5 1/5] libpq: Request protocol version 3.9999 to GREASE the
 ecosystem

The main reason that libpq does not request protocol version 3.2 by
default is because other proxy/server implementations don't implement
the negotiation. This is a bit of a chicken and egg problem: We don't
bump the default version that libpq asks, but proxies will only
implement version negotiation when their users run into issues.

This patch defines 3.999 as an explicitly unsupported protocol version
number and _pq_.test_protocol_negotiation as an explicitly unsupported
protocol extension. It also starts requesting that version and protocol
extension by default. This change to the default will be reverted before
we release PG19 release candidates (when exactly to revert before that
time is TBD). The intent is to stress test the ecosystem for
servers/middleware that don't support protocol version negotiation, so
that those servers/middleware can implement the negotiation. This is
similar to the GREASE[1] mechanism that TLS has.

It's still possible for users to connect to servers that don't support
protocol negotiation by using max_protocol_version=3.0 in their
connection string. Only the default connection behaviour is impacted.

[1]: https://www.rfc-editor.org/rfc/rfc8701.html

Author: Jelte Fennema-Nio <postgres@jeltef.nl>
---
 doc/src/sgml/libpq.sgml                       | 24 ++++++----
 doc/src/sgml/protocol.sgml                    | 43 +++++++++++++++--
 src/include/libpq/pqcomm.h                    |  9 ++++
 src/interfaces/libpq/fe-connect.c             | 20 ++++----
 src/interfaces/libpq/fe-protocol3.c           | 46 +++++++++++++++++--
 .../modules/libpq_pipeline/libpq_pipeline.c   |  6 +--
 6 files changed, 121 insertions(+), 27 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 21e1ba34a4e..fb6ce177f0d 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2213,15 +2213,21 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       <listitem>
        <para>
         Specifies the protocol version to request from the server.
-        The default is to use version <literal>3.0</literal> of the
-        <productname>PostgreSQL</productname> protocol, unless the connection
-        string specifies a feature that relies on a higher protocol version,
-        in which case the latest version supported by libpq is used. If the
-        server does not support the protocol version requested by the client,
-        the connection is automatically downgraded to a lower minor protocol
-        version that the server supports. After the connection attempt has
-        completed you can use <xref linkend="libpq-PQfullProtocolVersion"/> to
-        find out which exact protocol version was negotiated.
+        During the PostgreSQL 19 beta period, the default is to use
+        <literal>3.9999</literal>, a GREASE (Generate Random Extensions And
+        Sustain Extensibility) value that tests proper protocol negotiation
+        implementation. If the server does not support the protocol version
+        requested by the client, the connection is automatically downgraded to
+        a lower minor protocol version that the server supports. After the
+        connection attempt has completed you can use
+        <xref linkend="libpq-PQfullProtocolVersion"/> to find out which exact
+        protocol version was negotiated.
+       </para>
+
+       <para>
+        For servers that don't properly implement protocol version negotiation,
+        you can set <literal>max_protocol_version=3.0</literal> to connect
+        successfully.
        </para>
 
        <para>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index a2b528c481e..c61dd2bf948 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -192,10 +192,13 @@
    <title>Protocol Versions</title>
 
    <para>
-    The current, latest version of the protocol is version 3.2. However, for
-    backwards compatibility with old server versions and middleware that don't
-    support the version negotiation yet, libpq still uses protocol version 3.0
-    by default.
+    The current, latest version of the protocol is version 3.2. During the
+    PostgreSQL 19 beta period, libpq defaults to requesting protocol version
+    3.9999 to test that servers and middleware properly implement protocol
+    version negotiation. Servers that support negotiation will automatically
+    downgrade to version 3.2 or 3.0. For servers that don't support
+    negotiation, users can connect by explicitly setting
+    <literal>max_protocol_version=3.0</literal> in their connection string.
    </para>
 
    <para>
@@ -238,6 +241,20 @@
      </thead>
 
      <tbody>
+      <row>
+      <entry>3.9999</entry>
+      <entry>-</entry>
+      <entry>GREASE (Generate Random Extensions And Sustain Extensibility)
+        version. This version number is intentionally reserved and will never
+        be implemented. During the PostgreSQL 19 beta period, libpq requests
+        this version by default to test that servers and middleware properly
+        implement protocol version negotiation via
+        <literal>NegotiateProtocolVersion</literal>. Servers should respond
+        by downgrading to a supported version. This mechanism helps ensure
+        the ecosystem is ready for future protocol versions. libpq will revert
+        to defaulting to version 3.2 before the PostgreSQL 19 final release.
+      </entry>
+      </row>
       <row>
       <entry>3.2</entry>
       <entry>PostgreSQL 18 and later</entry>
@@ -6147,6 +6164,24 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
             </para>
            </listitem>
           </varlistentry>
+          <varlistentry>
+           <term><literal>_pq_.test_protocol_negotiation</literal></term>
+           <listitem>
+            <para>
+             A reserved protocol extension requested by libpq during the
+             PostgreSQL 19 beta period to test that servers properly implement
+             protocol version negotiation. When the client requests the GREASE
+             protocol version (3.9999), this parameter is automatically
+             included in the startup packet too. Servers should report it as
+             unsupported in their <literal>NegotiateProtocolVersion</literal>
+             response. In GREASE mode the connection will fail if the server
+             doesn't report this parameter as unsupported, ensuring
+             comprehensive implementation of protocol negotiation. This
+             parameter is reserved and will never actually be implemented by a
+             server.
+            </para>
+           </listitem>
+          </varlistentry>
          </variablelist>
 
          In addition to the above, other parameters may be listed.
diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index 1bbe5b9ee45..ca5127adfe5 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -104,6 +104,15 @@ is_unixsock_path(const char *path)
  */
 #define PG_PROTOCOL_RESERVED_31		PG_PROTOCOL(3,1)
 
+/*
+ * PG_PROTOCOL_GREASE is an intentionally unsupported protocol version used
+ * for GREASE (Generate Random Extensions And Sustain Extensibility). This
+ * helps ensure that servers properly implement protocol version negotiation
+ * via NegotiateProtocolVersion. Version 3.9999 was chosen to be safely within
+ * the valid range but unlikely to ever be implemented.
+ */
+#define PG_PROTOCOL_GREASE		PG_PROTOCOL(3,9999)
+
 /*
  * A client can send a cancel-current-operation request to the postmaster.
  * This is uglier than sending it directly to the client's backend, but it
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index a0d2f749811..9fcf094a36f 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -2142,15 +2142,12 @@ pqConnectOptions2(PGconn *conn)
 	else
 	{
 		/*
-		 * To not break connecting to older servers/poolers that do not yet
-		 * support NegotiateProtocolVersion, default to the 3.0 protocol at
-		 * least for a while longer. Except when min_protocol_version is set
-		 * to something larger, then we might as well default to the latest.
+		 * Default to the GREASE protocol version to test that servers
+		 * properly implement NegotiateProtocolVersion. The server will
+		 * automatically downgrade to a supported version. This will be
+		 * changed to a supported version before the PG19 release.
 		 */
-		if (conn->min_pversion > PG_PROTOCOL(3, 0))
-			conn->max_pversion = PG_PROTOCOL_LATEST;
-		else
-			conn->max_pversion = PG_PROTOCOL(3, 0);
+		conn->max_pversion = PG_PROTOCOL_GREASE;
 	}
 
 	if (conn->min_pversion > conn->max_pversion)
@@ -4386,6 +4383,13 @@ keep_going:						/* We will come back to here until there is
 					goto error_return;
 				}
 
+				if (conn->max_pversion == PG_PROTOCOL_GREASE &&
+					conn->pversion == PG_PROTOCOL_GREASE)
+				{
+					libpq_append_conn_error(conn, "server incorrectly accepted reserved GREASE protocol version 3.9999 without negotiation");
+					goto error_return;
+				}
+
 				/* Almost there now ... */
 				conn->status = CONNECTION_CHECK_TARGET;
 				goto keep_going;
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 103428033ef..4f78b88b3a8 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -1444,6 +1444,8 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 {
 	int			their_version;
 	int			num;
+	bool		found_test_protocol_negotiation;
+	bool		expect_test_protocol_negotiation;
 
 	if (pqGetInt(&their_version, 4, conn) != 0)
 		goto eof;
@@ -1471,6 +1473,13 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 		goto failure;
 	}
 
+	/* The GREASE protocol version is intentionally unsupported and reserved */
+	if (their_version == PG_PROTOCOL_GREASE)
+	{
+		libpq_append_conn_error(conn, "received invalid protocol negotiation message: server claimed to support reserved GREASE protocol version 3.9999");
+		goto failure;
+	}
+
 	if (num < 0)
 	{
 		libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported negative number of unsupported parameters");
@@ -1499,9 +1508,12 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 	conn->pversion = their_version;
 
 	/*
-	 * We don't currently request any protocol extensions, so we don't expect
-	 * the server to reply with any either.
+	 * Check that all expected unsupported parameters are reported by the
+	 * server.
 	 */
+	found_test_protocol_negotiation = false;
+	expect_test_protocol_negotiation = (conn->max_pversion == PG_PROTOCOL_GREASE);
+
 	for (int i = 0; i < num; i++)
 	{
 		if (pqGets(&conn->workBuffer, conn))
@@ -1513,7 +1525,27 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 			libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported unsupported parameter name without a \"%s\" prefix (\"%s\")", "_pq_.", conn->workBuffer.data);
 			goto failure;
 		}
-		libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported an unsupported parameter that was not requested (\"%s\")", conn->workBuffer.data);
+
+		/* Check if this is the expected test parameter */
+		if (expect_test_protocol_negotiation && strcmp(conn->workBuffer.data, "_pq_.test_protocol_negotiation") == 0)
+		{
+			found_test_protocol_negotiation = true;
+		}
+		else
+		{
+			libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported an unsupported parameter that was not requested (\"%s\")", conn->workBuffer.data);
+			goto failure;
+		}
+	}
+
+	/*
+	 * If we requested the GREASE protocol version, the server must report
+	 * _pq_.test_protocol_negotiation as unsupported. This ensures
+	 * comprehensive NegotiateProtocolVersion implementation.
+	 */
+	if (expect_test_protocol_negotiation && !found_test_protocol_negotiation)
+	{
+		libpq_append_conn_error(conn, "server did not report the unsupported `_pq_.test_protocol_negotiation` parameter in its protocol negotiation message");
 		goto failure;
 	}
 
@@ -2464,6 +2496,14 @@ build_startup_packet(const PGconn *conn, char *packet,
 	if (conn->client_encoding_initial && conn->client_encoding_initial[0])
 		ADD_STARTUP_OPTION("client_encoding", conn->client_encoding_initial);
 
+	/*
+	 * Add the test protocol negotiation option if we're using the GREASE
+	 * protocol version. This tests that servers properly report unsupported
+	 * protocol options in their NegotiateProtocolVersion response.
+	 */
+	if (conn->pversion == PG_PROTOCOL_GREASE)
+		ADD_STARTUP_OPTION("_pq_.test_protocol_negotiation", "");
+
 	/* Add any environment-driven GUC settings needed */
 	for (next_eo = options; next_eo->envName; next_eo++)
 	{
diff --git a/src/test/modules/libpq_pipeline/libpq_pipeline.c b/src/test/modules/libpq_pipeline/libpq_pipeline.c
index 0fb44be32ce..b819bcc273c 100644
--- a/src/test/modules/libpq_pipeline/libpq_pipeline.c
+++ b/src/test/modules/libpq_pipeline/libpq_pipeline.c
@@ -1363,7 +1363,7 @@ test_protocol_version(PGconn *conn)
 	Assert(max_protocol_version_index >= 0);
 
 	/*
-	 * Test default protocol_version
+	 * Test default protocol_version (GREASE - should negotiate down to 3.2)
 	 */
 	vals[max_protocol_version_index] = "";
 	conn = PQconnectdbParams(keywords, vals, false);
@@ -1373,8 +1373,8 @@ test_protocol_version(PGconn *conn)
 				 PQerrorMessage(conn));
 
 	protocol_version = PQfullProtocolVersion(conn);
-	if (protocol_version != 30000)
-		pg_fatal("expected 30000, got %d", protocol_version);
+	if (protocol_version != 30002)
+		pg_fatal("expected 30002, got %d", protocol_version);
 
 	PQfinish(conn);
 

base-commit: 9b9eaf08ab2dc22c691b22e59f1574e0f1bcc822
-- 
2.52.0

v5-0002-fixup-libpq-Request-protocol-version-3.9999-to-GR.patchtext/x-patch; charset=utf-8; name=v5-0002-fixup-libpq-Request-protocol-version-3.9999-to-GR.patchDownload
From 656ecd3b2b910d3e9f96e8a57323399a3e70a7bd Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Fri, 23 Jan 2026 23:13:00 +0100
Subject: [PATCH v5 2/5] fixup! libpq: Request protocol version 3.9999 to
 GREASE the ecosystem

---
 doc/src/sgml/protocol.sgml | 75 +++++++++++++++++++++++++++-----------
 1 file changed, 54 insertions(+), 21 deletions(-)

diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index c61dd2bf948..78d272ccc50 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -224,7 +224,7 @@
    </para>
 
    <para>
-    <xref linkend="protocol-versions-table"/> shows the currently supported
+    <xref linkend="protocol-versions-table"/> shows the currently defined
     protocol versions.
    </para>
 
@@ -252,7 +252,7 @@
         <literal>NegotiateProtocolVersion</literal>. Servers should respond
         by downgrading to a supported version. This mechanism helps ensure
         the ecosystem is ready for future protocol versions. libpq will revert
-        to defaulting to version 3.2 before the PostgreSQL 19 final release.
+        to defaulting to version 3.0 before the PostgreSQL 19 final release.
       </entry>
       </row>
       <row>
@@ -287,6 +287,56 @@
      </tbody>
     </tgroup>
    </table>
+
+  </sect2>
+
+  <sect2 id="protocol-extensions">
+   <title>Protocol Extensions</title>
+
+   <para>
+    In addition to protocol version negotiation, the client can request
+    optional protocol extensions by including parameters in the startup message.
+    Protocol extension parameter names must be prefixed with
+    <literal>_pq_.</literal> to distinguish them from server parameters.
+    If the server does not recognize an extension, it will report it in the
+    NegotiateProtocolVersion message. <xref
+    linkend="protocol-extensions-table"/> shows the currently defined
+    protocol extensions.
+   </para>
+
+   <table id="protocol-extensions-table">
+    <title>Protocol Extensions</title>
+
+    <tgroup cols="4">
+     <thead>
+      <row>
+       <entry>Extension</entry>
+       <entry>Supported Values</entry>
+       <entry>Introduced In</entry>
+       <entry>Description</entry>
+      </row>
+     </thead>
+
+     <tbody>
+      <row>
+       <entry><literal>test_protocol_negotiation</literal></entry>
+       <entry>-</entry>
+       <entry>-</entry>
+       <entry>
+        A reserved protocol extension requested by libpq to test that servers
+        properly implement protocol version negotiation. When the client
+        requests a GREASE protocol version, this parameter is automatically
+        included in the startup packet too. Servers should report it as
+        unsupported in their <literal>NegotiateProtocolVersion</literal>
+        response. In GREASE mode the connection will fail if the server
+        doesn't report this parameter as unsupported, ensuring comprehensive
+        implementation of protocol negotiation. This parameter is reserved and
+        will never actually be implemented by a server.
+       </entry>
+      </row>
+     </tbody>
+    </tgroup>
+   </table>
   </sect2>
  </sect1>
 
@@ -6164,29 +6214,12 @@ psql "dbname=postgres replication=database" -c "IDENTIFY_SYSTEM;"
             </para>
            </listitem>
           </varlistentry>
-          <varlistentry>
-           <term><literal>_pq_.test_protocol_negotiation</literal></term>
-           <listitem>
-            <para>
-             A reserved protocol extension requested by libpq during the
-             PostgreSQL 19 beta period to test that servers properly implement
-             protocol version negotiation. When the client requests the GREASE
-             protocol version (3.9999), this parameter is automatically
-             included in the startup packet too. Servers should report it as
-             unsupported in their <literal>NegotiateProtocolVersion</literal>
-             response. In GREASE mode the connection will fail if the server
-             doesn't report this parameter as unsupported, ensuring
-             comprehensive implementation of protocol negotiation. This
-             parameter is reserved and will never actually be implemented by a
-             server.
-            </para>
-           </listitem>
-          </varlistentry>
          </variablelist>
 
          In addition to the above, other parameters may be listed.
          Parameter names beginning with <literal>_pq_.</literal> are
-         reserved for use as protocol extensions, while others are
+         reserved for use as protocol extensions (see
+         <xref linkend="protocol-extensions"/>), while others are
          treated as run-time parameters to be set at backend start
          time.  Such settings will be applied during backend start
          (after parsing the command-line arguments if any) and will
-- 
2.52.0

v5-0003-Implement-more-complicated-grease.patchtext/x-patch; charset=utf-8; name=v5-0003-Implement-more-complicated-grease.patchDownload
From 3f074ffeaf9152c8a01350e2d5f69da0777c3f1c Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Thu, 15 Jan 2026 09:01:52 +0100
Subject: [PATCH v5 3/5] Implement more complicated grease

This actually implements the randomization that is part of the GREASE
acronym. When using max_protocol_version=grease, the version will now be
chosen from the range 3.9990-3.9999. The names of the reserved protocol
extensions are randomized too, as well as how many are requested
exactly.
---
 src/include/libpq/pqcomm.h          |  19 +++--
 src/interfaces/libpq/fe-connect.c   |  60 ++++++++++++--
 src/interfaces/libpq/fe-protocol3.c | 121 +++++++++++++++++++++++-----
 src/interfaces/libpq/libpq-int.h    |   3 +
 4 files changed, 167 insertions(+), 36 deletions(-)

diff --git a/src/include/libpq/pqcomm.h b/src/include/libpq/pqcomm.h
index ca5127adfe5..d38789bc35b 100644
--- a/src/include/libpq/pqcomm.h
+++ b/src/include/libpq/pqcomm.h
@@ -105,13 +105,20 @@ is_unixsock_path(const char *path)
 #define PG_PROTOCOL_RESERVED_31		PG_PROTOCOL(3,1)
 
 /*
- * PG_PROTOCOL_GREASE is an intentionally unsupported protocol version used
- * for GREASE (Generate Random Extensions And Sustain Extensibility). This
- * helps ensure that servers properly implement protocol version negotiation
- * via NegotiateProtocolVersion. Version 3.9999 was chosen to be safely within
- * the valid range but unlikely to ever be implemented.
+ * GREASE (Generate Random Extensions And Sustain Extensibility) version range.
+ * These are intentionally unsupported protocol versions used to ensure servers
+ * properly implement protocol version negotiation via NegotiateProtocolVersion.
+ * The range 3.9990-3.9999 was chosen to be safely within the valid range but
+ * unlikely to ever be implemented. A random version from this range is selected
+ * at connection time to prevent implementations from accidentally depending on
+ * specific GREASE values.
  */
-#define PG_PROTOCOL_GREASE		PG_PROTOCOL(3,9999)
+#define PG_PROTOCOL_GREASE_MIN	PG_PROTOCOL(3,9990)
+#define PG_PROTOCOL_GREASE_MAX	PG_PROTOCOL(3,9999)
+
+/* Check if a protocol version is in the GREASE range */
+#define PG_PROTOCOL_IS_GREASE(v) \
+	((v) >= PG_PROTOCOL_GREASE_MIN && (v) <= PG_PROTOCOL_GREASE_MAX)
 
 /*
  * A client can send a cancel-current-operation request to the postmaster.
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 9fcf094a36f..fdc05cd3243 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -2142,12 +2142,45 @@ pqConnectOptions2(PGconn *conn)
 	else
 	{
 		/*
-		 * Default to the GREASE protocol version to test that servers
-		 * properly implement NegotiateProtocolVersion. The server will
-		 * automatically downgrade to a supported version. This will be
-		 * changed to a supported version before the PG19 release.
+		 * Default to a GREASE protocol version to test that servers properly
+		 * implement NegotiateProtocolVersion. The server will automatically
+		 * downgrade to a supported version. This will be changed to a
+		 * supported version before the PG19 release.
 		 */
-		conn->max_pversion = PG_PROTOCOL_GREASE;
+		conn->max_pversion = PG_PROTOCOL_GREASE_MAX;
+	}
+
+	/*
+	 * If using GREASE, randomize the version, the number of GREASE parameters
+	 * (0-5), and each parameter's ID. This prevents implementations from
+	 * accidentally depending on specific GREASE values.
+	 */
+	if (PG_PROTOCOL_IS_GREASE(conn->max_pversion))
+	{
+		int			grease_range;
+
+		libpq_prng_init(conn);
+		grease_range = PG_PROTOCOL_MINOR(PG_PROTOCOL_GREASE_MAX) -
+			PG_PROTOCOL_MINOR(PG_PROTOCOL_GREASE_MIN);
+		conn->max_pversion = PG_PROTOCOL(3,
+										 PG_PROTOCOL_MINOR(PG_PROTOCOL_GREASE_MIN) +
+										 pg_prng_uint64_range(&conn->prng_state, 0, grease_range));
+		conn->ngrease_params = pg_prng_uint64_range(&conn->prng_state, 0, 5);
+
+		/* Initialize prefix indices and shuffle them (Fisher-Yates) */
+		for (int g = 0; g < 5; g++)
+			conn->grease_prefix[g] = g;
+		for (int g = 4; g > 0; g--)
+		{
+			int			j = pg_prng_uint64_range(&conn->prng_state, 0, g);
+			int			tmp = conn->grease_prefix[g];
+
+			conn->grease_prefix[g] = conn->grease_prefix[j];
+			conn->grease_prefix[j] = tmp;
+		}
+
+		for (int g = 0; g < conn->ngrease_params; g++)
+			conn->grease_params[g] = (uint16) pg_prng_uint64_range(&conn->prng_state, 0, 0xFFFF);
 	}
 
 	if (conn->min_pversion > conn->max_pversion)
@@ -4383,10 +4416,11 @@ keep_going:						/* We will come back to here until there is
 					goto error_return;
 				}
 
-				if (conn->max_pversion == PG_PROTOCOL_GREASE &&
-					conn->pversion == PG_PROTOCOL_GREASE)
+				if (PG_PROTOCOL_IS_GREASE(conn->max_pversion) &&
+					PG_PROTOCOL_IS_GREASE(conn->pversion))
 				{
-					libpq_append_conn_error(conn, "server incorrectly accepted reserved GREASE protocol version 3.9999 without negotiation");
+					libpq_append_conn_error(conn, "server incorrectly accepted reserved GREASE protocol version 3.%d without negotiation",
+											PG_PROTOCOL_MINOR(conn->pversion));
 					goto error_return;
 				}
 
@@ -8341,6 +8375,16 @@ pqParseProtocolVersion(const char *value, ProtocolVersion *result, PGconn *conn,
 		return true;
 	}
 
+	if (strcmp(value, "grease") == 0)
+	{
+		/*
+		 * Use a placeholder; the actual random version is selected later when
+		 * the PRNG is initialized.
+		 */
+		*result = PG_PROTOCOL_GREASE_MAX;
+		return true;
+	}
+
 	libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
 							context, value);
 	return false;
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index 4f78b88b3a8..c0e0142798a 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -31,6 +31,20 @@
 #include "mb/pg_wchar.h"
 #include "port/pg_bswap.h"
 
+/*
+ * GREASE parameter name prefixes. Each GREASE parameter uses a different
+ * prefix to prevent implementations from pattern-matching on a single prefix.
+ * The full parameter name is the prefix followed by a random 4-digit hex
+ * number, e.g. "_pq_.protocol_grease_a1b2".
+ */
+static const char *const grease_prefixes[5] = {
+	"_pq_.test_protocol_negotiation_",
+	"_pq_.negotiation_test_",
+	"_pq_.protocol_grease_",
+	"_pq_.grease_the_server_",
+	"_pq_.always_unknown_extension_"
+};
+
 /*
  * This macro lists the backend message types that could be "long" (more
  * than a couple of kilobytes).
@@ -1444,8 +1458,9 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 {
 	int			their_version;
 	int			num;
-	bool		found_test_protocol_negotiation;
-	bool		expect_test_protocol_negotiation;
+	int			ngrease_found;
+	bool		grease_found[5] = {false};
+	bool		expect_grease;
 
 	if (pqGetInt(&their_version, 4, conn) != 0)
 		goto eof;
@@ -1473,10 +1488,24 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 		goto failure;
 	}
 
-	/* The GREASE protocol version is intentionally unsupported and reserved */
-	if (their_version == PG_PROTOCOL_GREASE)
+	/* GREASE protocol versions are intentionally unsupported and reserved */
+	if (PG_PROTOCOL_IS_GREASE(their_version))
+	{
+		libpq_append_conn_error(conn, "received invalid protocol negotiation message: server claimed to support reserved GREASE protocol version 3.%d",
+								PG_PROTOCOL_MINOR(their_version));
+		goto failure;
+	}
+
+	/*
+	 * If the server responds with a version newer than what this libpq
+	 * supports, disconnect. This can happen if we sent a GREASE version and a
+	 * future server legitimately supports a newer minor version than us.
+	 */
+	if (their_version > PG_PROTOCOL_LATEST)
 	{
-		libpq_append_conn_error(conn, "received invalid protocol negotiation message: server claimed to support reserved GREASE protocol version 3.9999");
+		libpq_append_conn_error(conn, "server proposed protocol version 3.%d, but libpq only supports up to 3.%d",
+								PG_PROTOCOL_MINOR(their_version),
+								PG_PROTOCOL_MINOR(PG_PROTOCOL_LATEST));
 		goto failure;
 	}
 
@@ -1509,13 +1538,16 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 
 	/*
 	 * Check that all expected unsupported parameters are reported by the
-	 * server.
+	 * server. In GREASE mode, we expect all our GREASE parameters to be
+	 * reported as unsupported.
 	 */
-	found_test_protocol_negotiation = false;
-	expect_test_protocol_negotiation = (conn->max_pversion == PG_PROTOCOL_GREASE);
+	ngrease_found = 0;
+	expect_grease = PG_PROTOCOL_IS_GREASE(conn->max_pversion);
 
 	for (int i = 0; i < num; i++)
 	{
+		bool		matched = false;
+
 		if (pqGets(&conn->workBuffer, conn))
 		{
 			goto eof;
@@ -1526,12 +1558,31 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 			goto failure;
 		}
 
-		/* Check if this is the expected test parameter */
-		if (expect_test_protocol_negotiation && strcmp(conn->workBuffer.data, "_pq_.test_protocol_negotiation") == 0)
+		/* Check if this matches any of our expected GREASE parameters */
+		if (expect_grease)
 		{
-			found_test_protocol_negotiation = true;
+			for (int j = 0; j < conn->ngrease_params; j++)
+			{
+				char		expected_opt[32];
+
+				snprintf(expected_opt, sizeof(expected_opt), "%s%04x",
+						 grease_prefixes[conn->grease_prefix[j]], conn->grease_params[j]);
+				if (strcmp(conn->workBuffer.data, expected_opt) == 0)
+				{
+					if (grease_found[j])
+					{
+						libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported duplicate unsupported parameter (\"%s\")", conn->workBuffer.data);
+						goto failure;
+					}
+					grease_found[j] = true;
+					ngrease_found++;
+					matched = true;
+					break;
+				}
+			}
 		}
-		else
+
+		if (!matched)
 		{
 			libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported an unsupported parameter that was not requested (\"%s\")", conn->workBuffer.data);
 			goto failure;
@@ -1539,13 +1590,28 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 	}
 
 	/*
-	 * If we requested the GREASE protocol version, the server must report
-	 * _pq_.test_protocol_negotiation as unsupported. This ensures
-	 * comprehensive NegotiateProtocolVersion implementation.
+	 * If we requested a GREASE protocol version, the server must report all
+	 * our GREASE options as unsupported. This ensures comprehensive
+	 * NegotiateProtocolVersion implementation.
 	 */
-	if (expect_test_protocol_negotiation && !found_test_protocol_negotiation)
+	if (expect_grease && ngrease_found != conn->ngrease_params)
 	{
-		libpq_append_conn_error(conn, "server did not report the unsupported `_pq_.test_protocol_negotiation` parameter in its protocol negotiation message");
+		PQExpBufferData missing;
+
+		initPQExpBuffer(&missing);
+		for (int j = 0; j < conn->ngrease_params; j++)
+		{
+			if (!grease_found[j])
+			{
+				if (missing.len > 0)
+					appendPQExpBufferStr(&missing, ", ");
+				appendPQExpBuffer(&missing, "%s%04x",
+								  grease_prefixes[conn->grease_prefix[j]], conn->grease_params[j]);
+			}
+		}
+		libpq_append_conn_error(conn, "server did not report unsupported GREASE parameter(s): %s",
+								missing.data);
+		termPQExpBuffer(&missing);
 		goto failure;
 	}
 
@@ -2497,12 +2563,23 @@ build_startup_packet(const PGconn *conn, char *packet,
 		ADD_STARTUP_OPTION("client_encoding", conn->client_encoding_initial);
 
 	/*
-	 * Add the test protocol negotiation option if we're using the GREASE
-	 * protocol version. This tests that servers properly report unsupported
-	 * protocol options in their NegotiateProtocolVersion response.
+	 * Add GREASE protocol options if we're using a GREASE protocol version.
+	 * This tests that servers properly report unsupported protocol options in
+	 * their NegotiateProtocolVersion response. Each option name includes a
+	 * random suffix to prevent implementations from depending on specific
+	 * GREASE values. The number of options (0-5) is also randomized.
 	 */
-	if (conn->pversion == PG_PROTOCOL_GREASE)
-		ADD_STARTUP_OPTION("_pq_.test_protocol_negotiation", "");
+	if (PG_PROTOCOL_IS_GREASE(conn->pversion))
+	{
+		for (int i = 0; i < conn->ngrease_params; i++)
+		{
+			char		grease_opt[32];
+
+			snprintf(grease_opt, sizeof(grease_opt), "%s%04x",
+					 grease_prefixes[conn->grease_prefix[i]], conn->grease_params[i]);
+			ADD_STARTUP_OPTION(grease_opt, "");
+		}
+	}
 
 	/* Add any environment-driven GUC settings needed */
 	for (next_eo = options; next_eo->envName; next_eo++)
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index fb6a7cbf15d..a599d57e325 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -547,6 +547,9 @@ struct pg_conn
 	uint8	   *scram_server_key_binary;	/* binary SCRAM server key */
 	ProtocolVersion min_pversion;	/* protocol version to request */
 	ProtocolVersion max_pversion;	/* protocol version to request */
+	int			ngrease_params; /* number of GREASE parameters (0-5) */
+	int			grease_prefix[5];	/* which prefix to use for each param */
+	uint16		grease_params[5];	/* random IDs for GREASE parameter names */
 
 	/* Miscellaneous stuff */
 	int			be_pid;			/* PID of backend --- needed for cancels */
-- 
2.52.0

v5-0004-libpq-Reconnect-if-requested-grease-but-version-i.patchtext/x-patch; charset=utf-8; name=v5-0004-libpq-Reconnect-if-requested-grease-but-version-i.patchDownload
From 60ed7f5624b007b7ab7b27167fa44f0aa51692ab Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Thu, 15 Jan 2026 09:58:07 +0100
Subject: [PATCH v5 4/5] libpq: Reconnect if requested grease but version is
 newer

While max_protocol_version=grease is mostly meant for testing, people
might still use it in production environments. So it seems better to
support reconnecting automatically if the server responds with a
downgrade to a higher version than libpq supports.
---
 src/interfaces/libpq/fe-connect.c   | 23 ++++++++++++++++---
 src/interfaces/libpq/fe-protocol3.c | 34 +++++++++++++++++++----------
 2 files changed, 43 insertions(+), 14 deletions(-)

diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index fdc05cd3243..738655488f3 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -4196,10 +4196,27 @@ keep_going:						/* We will come back to here until there is
 						libpq_append_conn_error(conn, "received duplicate protocol negotiation message");
 						goto error_return;
 					}
-					if (pqGetNegotiateProtocolVersion3(conn))
 					{
-						/* pqGetNegotiateProtocolVersion3 set error already */
-						goto error_return;
+						int			rc = pqGetNegotiateProtocolVersion3(conn);
+
+						if (rc == 1)
+						{
+							/*
+							 * pqGetNegotiateProtocolVersion3 set error
+							 * already
+							 */
+							goto error_return;
+						}
+						if (rc == 2)
+						{
+							/*
+							 * Server proposed newer version than libpq
+							 * supports; retry with the latest supported
+							 * version.
+							 */
+							need_new_connection = true;
+							goto keep_going;
+						}
 					}
 					conn->pversion_negotiated = true;
 
diff --git a/src/interfaces/libpq/fe-protocol3.c b/src/interfaces/libpq/fe-protocol3.c
index c0e0142798a..b7e30516516 100644
--- a/src/interfaces/libpq/fe-protocol3.c
+++ b/src/interfaces/libpq/fe-protocol3.c
@@ -1452,6 +1452,8 @@ reportErrorPosition(PQExpBuffer msg, const char *query, int loc, int encoding)
  * Entry: 'v' message type and length have already been consumed.
  * Exit: returns 0 if successfully consumed message.
  *		 returns 1 on failure. The error message is filled in.
+ *		 returns 2 if server proposed a newer version and we were using GREASE;
+ *		   caller should retry the connection with the latest supported version.
  */
 int
 pqGetNegotiateProtocolVersion3(PGconn *conn)
@@ -1465,6 +1467,21 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 	if (pqGetInt(&their_version, 4, conn) != 0)
 		goto eof;
 
+	/*
+	 * If the server responds with a version newer than what this libpq
+	 * supports, and we were using GREASE, retry with the latest supported
+	 * version. This can happen if a future server legitimately supports a
+	 * newer minor version than us. We don't try to parse the rest of the
+	 * message since we don't know what format a newer protocol version might
+	 * use. The GREASE checks for protocol extensions will still happen on the
+	 * retry.
+	 */
+	if (their_version > PG_PROTOCOL_LATEST && PG_PROTOCOL_IS_GREASE(conn->pversion))
+	{
+		conn->max_pversion = PG_PROTOCOL_LATEST;
+		return 2;
+	}
+
 	if (pqGetInt(&num, 4, conn) != 0)
 		goto eof;
 
@@ -1496,22 +1513,17 @@ pqGetNegotiateProtocolVersion3(PGconn *conn)
 		goto failure;
 	}
 
-	/*
-	 * If the server responds with a version newer than what this libpq
-	 * supports, disconnect. This can happen if we sent a GREASE version and a
-	 * future server legitimately supports a newer minor version than us.
-	 */
-	if (their_version > PG_PROTOCOL_LATEST)
+	if (num < 0)
 	{
-		libpq_append_conn_error(conn, "server proposed protocol version 3.%d, but libpq only supports up to 3.%d",
-								PG_PROTOCOL_MINOR(their_version),
-								PG_PROTOCOL_MINOR(PG_PROTOCOL_LATEST));
+		libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported negative number of unsupported parameters");
 		goto failure;
 	}
 
-	if (num < 0)
+	if (their_version > PG_PROTOCOL_LATEST)
 	{
-		libpq_append_conn_error(conn, "received invalid protocol negotiation message: server reported negative number of unsupported parameters");
+		libpq_append_conn_error(conn, "server proposed protocol version 3.%d, but libpq only supports up to 3.%d",
+								PG_PROTOCOL_MINOR(their_version),
+								PG_PROTOCOL_MINOR(PG_PROTOCOL_LATEST));
 		goto failure;
 	}
 
-- 
2.52.0

v5-0005-Update-libpq-docs-for-better-grease.patchtext/x-patch; charset=utf-8; name=v5-0005-Update-libpq-docs-for-better-grease.patchDownload
From ec791d56482888a5a52171c73c42aa32ac300d0d Mon Sep 17 00:00:00 2001
From: Jelte Fennema-Nio <postgres@jeltef.nl>
Date: Fri, 23 Jan 2026 22:32:20 +0100
Subject: [PATCH v5 5/5] Update libpq docs for better grease

Kept separate from the previous two patches for now while we discuss the
actual details of the implementation.
---
 doc/src/sgml/libpq.sgml    | 26 ++++++++++-----
 doc/src/sgml/protocol.sgml | 68 ++++++++++++++++++++++++--------------
 2 files changed, 62 insertions(+), 32 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index fb6ce177f0d..d785f18914f 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -2213,10 +2213,17 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       <listitem>
        <para>
         Specifies the protocol version to request from the server.
-        During the PostgreSQL 19 beta period, the default is to use
-        <literal>3.9999</literal>, a GREASE (Generate Random Extensions And
-        Sustain Extensibility) value that tests proper protocol negotiation
-        implementation. If the server does not support the protocol version
+        During the PostgreSQL 19 beta period, the default is
+        <literal>grease</literal>, which requests a random protocol version in
+        the range 3.9990-3.9999 and includes 0-5 random GREASE startup
+        parameters. This tests that servers and middleware properly implement
+        protocol version negotiation. The default will be changed to
+        <literal>latest</literal> before PostgreSQL 19 is released, but the
+        <literal>grease</literal> option will remain available for testing.
+       </para>
+
+       <para>
+        If the server does not support the protocol version
         requested by the client, the connection is automatically downgraded to
         a lower minor protocol version that the server supports. After the
         connection attempt has completed you can use
@@ -2232,10 +2239,13 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
 
        <para>
         The current supported values are
-        <literal>3.0</literal>, <literal>3.2</literal>,
-        and <literal>latest</literal>. The <literal>latest</literal> value is
-        equivalent to the latest protocol version supported by the libpq
-        version being used, which is currently <literal>3.2</literal>.
+        <literal>3.0</literal>,
+        <literal>3.2</literal>,
+        <literal>latest</literal>, and
+        <literal>grease</literal>.
+        The <literal>latest</literal> value is equivalent to the latest
+        protocol version supported by the libpq version being used, which is
+        currently <literal>3.2</literal>.
        </para>
       </listitem>
      </varlistentry>
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 78d272ccc50..922b7ca3b1d 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -193,12 +193,13 @@
 
    <para>
     The current, latest version of the protocol is version 3.2. During the
-    PostgreSQL 19 beta period, libpq defaults to requesting protocol version
-    3.9999 to test that servers and middleware properly implement protocol
-    version negotiation. Servers that support negotiation will automatically
-    downgrade to version 3.2 or 3.0. For servers that don't support
-    negotiation, users can connect by explicitly setting
-    <literal>max_protocol_version=3.0</literal> in their connection string.
+    PostgreSQL 19 beta period, libpq defaults to requesting a random protocol
+    version in the range 3.9990-3.9999 (the GREASE range) to test that servers
+    and middleware properly implement protocol version negotiation. Servers
+    that support negotiation will automatically downgrade to version 3.2 or
+    3.0. For servers that don't support negotiation, users can connect by
+    explicitly setting <literal>max_protocol_version=3.0</literal> in their
+    connection string.
    </para>
 
    <para>
@@ -242,17 +243,17 @@
 
      <tbody>
       <row>
-      <entry>3.9999</entry>
+      <entry>3.9990-3.9999</entry>
       <entry>-</entry>
       <entry>GREASE (Generate Random Extensions And Sustain Extensibility)
-        version. This version number is intentionally reserved and will never
-        be implemented. During the PostgreSQL 19 beta period, libpq requests
-        this version by default to test that servers and middleware properly
-        implement protocol version negotiation via
+        version range. These version numbers are intentionally reserved and
+        will never be implemented. Clients can request a random version from
+        this range to test that servers and middleware properly implement
+        protocol version negotiation via
         <literal>NegotiateProtocolVersion</literal>. Servers should respond
         by downgrading to a supported version. This mechanism helps ensure
-        the ecosystem is ready for future protocol versions. libpq will revert
-        to defaulting to version 3.0 before the PostgreSQL 19 final release.
+        the ecosystem is ready for future protocol versions. libpq supports
+        this via <literal>max_protocol_version=grease</literal>.
       </entry>
       </row>
       <row>
@@ -319,21 +320,40 @@
 
      <tbody>
       <row>
-       <entry><literal>test_protocol_negotiation</literal></entry>
+       <entry><literal>test_protocol_negotiation_XXXX</literal></entry>
        <entry>-</entry>
        <entry>-</entry>
-       <entry>
-        A reserved protocol extension requested by libpq to test that servers
-        properly implement protocol version negotiation. When the client
-        requests a GREASE protocol version, this parameter is automatically
-        included in the startup packet too. Servers should report it as
-        unsupported in their <literal>NegotiateProtocolVersion</literal>
-        response. In GREASE mode the connection will fail if the server
-        doesn't report this parameter as unsupported, ensuring comprehensive
-        implementation of protocol negotiation. This parameter is reserved and
-        will never actually be implemented by a server.
+       <entry morerows="4">
+        Reserved GREASE (Generate Random Extensions And Sustain Extensibility)
+        protocol extensions. The XXXX can be replaced for any random hex
+        number. These protocol extensions will never be implemented by a
+        server, and they should thus report them as unsupported in their
+        <literal>NegotiateProtocolVersion</literal> response. Clients are free
+        to request them to ensure a comprehensive implementation of the
+        protocol negotiation. As an example libpq sends 0-5 of these parameters
+        when <literal>max_protocol_version=grease</literal>.
        </entry>
       </row>
+      <row>
+       <entry><literal>negotiation_test_XXXX</literal></entry>
+       <entry>-</entry>
+       <entry>-</entry>
+      </row>
+      <row>
+       <entry><literal>protocol_grease_XXXX</literal></entry>
+       <entry>-</entry>
+       <entry>-</entry>
+      </row>
+      <row>
+       <entry><literal>grease_the_server_XXXX</literal></entry>
+       <entry>-</entry>
+       <entry>-</entry>
+      </row>
+      <row>
+       <entry><literal>always_unknown_extension_XXXX</literal></entry>
+       <entry>-</entry>
+       <entry>-</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
-- 
2.52.0

#17Jacob Champion
jacob.champion@enterprisedb.com
In reply to: Jelte Fennema-Nio (#16)
Re: libpq: Bump protocol version to version 3.2 at least until the first/second beta

On Fri, Jan 23, 2026 at 3:40 PM Jelte Fennema-Nio <postgres@jeltef.nl> wrote:

I saw that you committed a few patches. Given that you said you wanted
to wordsmith the last one, this seemed like a good time to send in some
of my own improvements in that regard.

(Well, that was impressive timing.)

--Jacob