On Wed, Jan 14, 2026 at 2:56 PM Jacob Champion
<[email protected]> 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
<[email protected]> 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
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", "");

Attachment: v5-0001-doc-Expand-upon-protocol-versions-and-extensions.patch
Description: Binary data

Attachment: v5-0002-libpq-Prepare-for-protocol-grease-during-19beta.patch
Description: Binary data

Attachment: v5-0003-libpq-Grease-the-protocol-by-default.patch
Description: Binary data

Reply via email to