From f7bc17c458f5718f76f102b17aaa93b94220fb6f 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 v6 3/4] 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 303aeae7b6b..89503571e9e 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 ce1a9995f46..409b3a7fa45 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

