From 7a0f8f81fe78cc4489fe272827e0328e1db29910 Mon Sep 17 00:00:00 2001
From: Greg Sabino Mullane <greg@turnstep.com>
Date: Sun, 6 Sep 2026 21:44:10 -0400
Subject: [PATCH] Allow specific information to be output directly by Postgres.

---
 doc/src/sgml/config.sgml                      |  62 ++++
 src/backend/tcop/backend_startup.c            | 334 ++++++++++++++++++
 src/backend/utils/misc/guc_parameters.dat     |  10 +
 src/backend/utils/misc/postgresql.conf.sample |   7 +
 src/include/tcop/backend_startup.h            |   2 +
 src/include/utils/guc_hooks.h                 |   2 +
 src/test/modules/test_misc/meson.build        |   1 +
 .../test_misc/t/016_expose_information.pl     | 179 ++++++++++
 8 files changed, 597 insertions(+)
 create mode 100644 src/test/modules/test_misc/t/016_expose_information.pl

diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 0165eb9ec02..7977ee252cc 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -661,6 +661,68 @@ include_dir 'conf.d'
 
      <variablelist>
 
+     <varlistentry id="guc-expose-information" xreflabel="expose_information">
+      <term><varname>expose_information</varname> (<type>string</type>)
+      <indexterm>
+       <primary><varname>expose_information</varname> configuration parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Allows for specific information to be returned from the servers without
+        requiring a login. Requests should come in as a simple HTTP request as a
+        GET or HEAD to the PostgreSQL port.
+        The default is the empty string, <literal>''</literal>, which
+        prevents any information from being output. The following options may be
+        specified alone or in a comma-separated list:
+       </para>
+
+       <table id="expose-information-options">
+        <title>Expose Information Options</title>
+        <tgroup cols="2">
+         <colspec colname="col1" colwidth="1*"/>
+         <colspec colname="col2" colwidth="2*"/>
+         <thead>
+          <row>
+           <entry>Name</entry>
+           <entry>Description</entry>
+          </row>
+         </thead>
+         <tbody>
+          <row>
+           <entry><literal>role</literal></entry>
+           <entry>Determine if the server is in recovery mode or not. If the request is
+             <literal>HEAD /replica</literal>, then an HTTP response code of 200 (yes it is a replica)
+             or 503 (not a replica) is returned. A request of <literal>HEAD /primary</literal>
+             returns the opposite values. This can be used as a drop-in replacement for the same
+             functionality provided by the Patroni program. For the request <literal>GET /replica</literal>
+             and <literal>GET /primary</literal> the string <literal>1</literal> or <literal>0</literal>
+             is returned.</entry>
+          </row>
+
+          <row>
+           <entry><literal>sysid</literal></entry>
+           <entry>Returns the system identifier of the server. This can be useful to determine if the underlying
+             server has changed, as the initdb program will always generate a new system identifier.
+             For the request <literal>GET /sysid</literal> the string <literal>12345</literal>
+             is returned, in which "12345" will be the specific system identifier
+             (typically a 20-digit number)</entry>
+          </row>
+
+          <row>
+           <entry><literal>version</literal></entry>
+           <entry>Returns the current version of the server. Specifically, the value of
+             <literal>server_version_num</literal>. For the request <literal>GET /version</literal>
+             the string <literal>200002</literal> is returned (for this example,
+             the version of Postgres is 20.2)</entry>
+          </row>
+         </tbody>
+        </tgroup>
+       </table>
+
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-listen-addresses" xreflabel="listen_addresses">
       <term><varname>listen_addresses</varname> (<type>string</type>)
       <indexterm>
diff --git a/src/backend/tcop/backend_startup.c b/src/backend/tcop/backend_startup.c
index 912ad7dc957..a03af3d20ac 100644
--- a/src/backend/tcop/backend_startup.c
+++ b/src/backend/tcop/backend_startup.c
@@ -46,6 +46,39 @@
 bool		Trace_connection_negotiation = false;
 uint32		log_connections = 0;
 char	   *log_connections_string = NULL;
+uint32		expose_information = 0;
+char	   *expose_information_string = NULL;
+
+/* Expose information bitmap */
+#define EXPOSE_INFO_ROLE	 1
+#define EXPOSE_INFO_SYSID	 2
+#define EXPOSE_INFO_VERSION  4
+
+#define EXPOSE_MIN_QUERY 10		/* Shortest possible line: "GET /sysid" */
+#define EXPOSE_MAX_QUERY 16		/* Longest possible GET line */
+
+#define EXPOSE_SEND_TIMEOUT_MS 2000 /* Maximum time to wait for clients to
+									 * read info (milliseconds) */
+#define EXPOSE_SEND_RETRY_SLEEP_US 10000	/* How long to sleep between
+											 * send() calls (microseconds) */
+
+typedef enum
+{
+	EXPOSE_TYPE_NOTHING,
+	EXPOSE_TYPE_HEAD_REPLICA,
+	EXPOSE_TYPE_GET_REPLICA,
+	EXPOSE_TYPE_HEAD_PRIMARY,
+	EXPOSE_TYPE_GET_PRIMARY,
+	EXPOSE_TYPE_GET_SYSID,
+	EXPOSE_TYPE_GET_VERSION,
+}			ExposeReturnType;
+
+typedef struct
+{
+	const char *endpoint;
+	int			require;
+	ExposeReturnType type;
+}			ExposeEndpointAction;
 
 /* Other globals */
 
@@ -65,6 +98,7 @@ static void SendNegotiateProtocolVersion(List *unrecognized_protocol_options);
 static void process_startup_packet_die(SIGNAL_ARGS);
 static void StartupPacketTimeoutHandler(void);
 static bool validate_log_connections_options(List *elemlist, uint32 *flags);
+static bool ExposeInformation(pgsocket fd);
 
 /*
  * Entry point for a new backend process.
@@ -148,6 +182,15 @@ BackendInitialize(ClientSocket *client_sock, CAC_state cac)
 	StringInfoData ps_data;
 	MemoryContext oldcontext;
 
+	/*
+	 * Scan for a simple GET / HEAD request. If this is detected and handled,
+	 * we are done and can immediately exit.
+	 */
+	if ((expose_information > 0)
+		&& ExposeInformation(client_sock->sock))
+		_exit(0);				/* Safe to use exit: no state or resources
+								 * created yet */
+
 	/* Tell fd.c about the long-lived FD associated with the client_sock */
 	ReserveExternalFD();
 
@@ -1108,6 +1151,72 @@ next:	;
 }
 
 
+/*
+ * GUC check_hook for expose_information
+ */
+bool
+check_expose_information(char **newval, void **extra, GucSource source)
+{
+	char	   *rawstring;
+	List	   *elemlist;
+	ListCell   *l;
+	int			newexpose = 0;
+	int		   *myextra;
+
+	/* Need a modifiable copy of string */
+	rawstring = pstrdup(*newval);
+
+	/* Parse string into list of identifiers */
+	if (!SplitIdentifierString(rawstring, ',', &elemlist))
+	{
+		/* syntax error in list */
+		GUC_check_errdetail("List syntax is invalid.");
+		pfree(rawstring);
+		list_free(elemlist);
+		return false;
+	}
+
+	foreach(l, elemlist)
+	{
+		char	   *tok = (char *) lfirst(l);
+
+		if (pg_strcasecmp(tok, "role") == 0)
+			newexpose |= EXPOSE_INFO_ROLE;
+		else if (pg_strcasecmp(tok, "sysid") == 0)
+			newexpose |= EXPOSE_INFO_SYSID;
+		else if (pg_strcasecmp(tok, "version") == 0)
+			newexpose |= EXPOSE_INFO_VERSION;
+		else
+		{
+			GUC_check_errdetail("Unrecognized key word: \"%s\".", tok);
+			pfree(rawstring);
+			list_free(elemlist);
+			return false;
+		}
+	}
+
+	pfree(rawstring);
+	list_free(elemlist);
+
+	myextra = (int *) guc_malloc(LOG, sizeof(int));
+	if (!myextra)
+		return false;
+	*myextra = newexpose;
+	*extra = myextra;
+
+	return true;
+}
+
+/*
+ * GUC assign_hook for expose_information
+ */
+void
+assign_expose_information(const char *newval, void *extra)
+{
+	expose_information = *((int *) extra);
+}
+
+
 /*
  * GUC check hook for log_connections
  */
@@ -1160,3 +1269,228 @@ assign_log_connections(const char *newval, void *extra)
 {
 	log_connections = *((int *) extra);
 }
+
+/*
+ * ExposeInformation
+ *
+ * Handle early socket probe before full backend startup.
+ * Responds to small set of predefined endpoints (e.g. GET /replica)
+ *
+ * Requires the expose_information GUC to be non-empty
+ *
+ * Returns true if any endpoint is recognized.
+ */
+
+static bool
+ExposeInformation(pgsocket fd)
+{
+	ssize_t		n;
+	char		buf[EXPOSE_MAX_QUERY + 1];
+	ExposeReturnType type;
+	bool		result = false;
+#ifdef WIN32
+	int			save_win32_noblock = pgwin32_noblock;
+#endif
+
+	/* Matching string, required setting, type of response */
+	static const ExposeEndpointAction endpoint_actions[] =
+	{
+		{
+			"HEAD /replica", EXPOSE_INFO_ROLE, EXPOSE_TYPE_HEAD_REPLICA
+		},
+		{
+			"GET /replica", EXPOSE_INFO_ROLE, EXPOSE_TYPE_GET_REPLICA
+		},
+		{
+			"HEAD /primary", EXPOSE_INFO_ROLE, EXPOSE_TYPE_HEAD_PRIMARY
+		},
+		{
+			"GET /primary", EXPOSE_INFO_ROLE, EXPOSE_TYPE_GET_PRIMARY
+		},
+		{
+			"GET /sysid", EXPOSE_INFO_SYSID, EXPOSE_TYPE_GET_SYSID
+		},
+		{
+			"GET /version", EXPOSE_INFO_VERSION, EXPOSE_TYPE_GET_VERSION
+		},
+	};
+
+	Assert(expose_information > 0);
+
+#ifdef WIN32
+	pgwin32_noblock = true;
+#else
+	if (!pg_set_noblock(fd))
+		goto cleanup;
+#endif
+
+	do
+	{
+		n = recv(fd, buf, EXPOSE_MAX_QUERY, MSG_PEEK);
+	} while (n < 0 && errno == EINTR);
+
+	/*
+	 * If there was a problem (n == -1), or the input is too short, we simply
+	 * leave and let the normal flow continue.
+	 */
+	if (n < EXPOSE_MIN_QUERY)
+		goto cleanup;
+
+	buf[n] = '\0';
+
+	type = EXPOSE_TYPE_NOTHING;
+	for (int i = 0; i < lengthof(endpoint_actions); i++)
+	{
+		size_t		endpoint_len = strlen(endpoint_actions[i].endpoint);
+
+		if (
+			(expose_information & endpoint_actions[i].require)
+			&&
+			strncmp(buf, endpoint_actions[i].endpoint, endpoint_len) == 0
+			&&
+			(buf[endpoint_len] == ' ' || buf[endpoint_len] == '\r' || buf[endpoint_len] == '\0')
+			)
+		{
+			type = endpoint_actions[i].type;
+			break;
+		}
+	}
+	if (type == EXPOSE_TYPE_NOTHING)
+		goto cleanup;
+
+	/* From this point onwards, we return true, as we have found a match */
+	result = true;
+
+	{
+		static const char http_version[] = "HTTP/1.1";
+		static const char http_type[] = "Content-Type: text/plain";
+		static const char http_conn[] = "Connection: close";
+		static const char http_len[] = "Content-Length";
+
+		/* Total bytes of above: 8 + 24 + 17 + 14 = 63 bytes */
+
+		StringInfoData msg;
+
+		TimestampTz start_time = 0;
+		size_t		sent = 0;
+		bool		send_failed = false;
+
+		if (type == EXPOSE_TYPE_HEAD_REPLICA || type == EXPOSE_TYPE_HEAD_PRIMARY)
+		{
+			/*
+			 * Caller only cares about the HTTP response code, so no content
+			 * needed
+			 */
+
+			bool		recovery_in_progress = RecoveryInProgress();
+
+			initStringInfoExt(&msg, 90);
+
+			appendStringInfo(&msg,
+							 "%s %s\r\n"
+							 "%s\r\n"
+							 "%s\r\n\r\n",
+							 http_version,
+							 (((recovery_in_progress && type == EXPOSE_TYPE_HEAD_REPLICA)
+							   || (!recovery_in_progress && type == EXPOSE_TYPE_HEAD_PRIMARY))
+							  ? "200 OK" : "503 Service Unavailable"),
+							 http_type,
+							 http_conn
+				);
+		}
+		else
+		{
+			StringInfoData content;
+
+			initStringInfoExt(&content, MAXINT8LEN + 3);
+
+			switch (type)
+			{
+
+				case EXPOSE_TYPE_GET_SYSID:
+					appendStringInfo(&content, UINT64_FORMAT "\r\n",
+									 GetSystemIdentifier());
+					break;
+				case EXPOSE_TYPE_GET_VERSION:
+					appendStringInfo(&content, "%d\r\n",
+									 PG_VERSION_NUM);
+					break;
+				case EXPOSE_TYPE_GET_REPLICA:
+					appendStringInfo(&content, "%d\r\n",
+									 RecoveryInProgress() ? 1 : 0);
+					break;
+				case EXPOSE_TYPE_GET_PRIMARY:
+					appendStringInfo(&content, "%d\r\n",
+									 RecoveryInProgress() ? 0 : 1);
+					break;
+				default:
+					elog(ERROR, "unrecognized ExposeReturnType: %d", (int) type);
+			}
+
+			initStringInfoExt(&msg, 128);
+			appendStringInfo(&msg,
+							 "%s 200 OK\r\n"
+							 "%s\r\n"
+							 "%s: %d\r\n"
+							 "%s\r\n\r\n"
+							 "%s",
+							 http_version,
+							 http_type,
+							 http_len, content.len,
+							 http_conn,
+							 content.data
+				);
+
+			pfree(content.data);
+		}
+
+		/*
+		 * Send the response. As this is at an early and important point, we
+		 * want to quickly close the connection if the client stops reading,
+		 * so we add a timeout.
+		 */
+
+		while (sent < (size_t) msg.len && !send_failed)
+		{
+
+			n = send(fd, msg.data + sent, msg.len - sent, 0);
+
+			if (n > 0)
+			{
+				sent += n;
+				continue;
+			}
+
+			/* While n==0 should not happen, we treat as a retry as well */
+			if (n == 0 ||
+				(n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)))
+			{
+				if (start_time == 0)
+					start_time = GetCurrentTimestamp();
+				else if (TimestampDifferenceExceeds(start_time, GetCurrentTimestamp(),
+													EXPOSE_SEND_TIMEOUT_MS))
+					send_failed = true;
+				else
+					pg_usleep(EXPOSE_SEND_RETRY_SLEEP_US);
+
+				continue;
+			}
+
+			/* The send() call failed in some way we cannot handle */
+			elog(LOG, "failed to send information to client: %m");
+			send_failed = true;
+
+		}
+
+		pfree(msg.data);
+
+	}
+
+cleanup:
+#ifdef WIN32
+	pgwin32_noblock = save_win32_noblock;
+#else
+	(void) pg_set_block(fd);
+#endif
+	return result;
+}
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 3c5e16ad1e7..84fabc54795 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -1066,6 +1066,16 @@
   boot_val => 'false',
 },
 
+{ name => 'expose_information', type => 'string', context => 'PGC_SIGHUP', group => 'CONN_AUTH_AUTH',
+  short_desc => 'Expose limited information without needing to login',
+  long_desc => 'Valid values are combinations of "role", "sysid", and "version"',
+  flags => 'GUC_LIST_INPUT',
+  variable => 'expose_information_string',
+  boot_val => '""',
+  check_hook => 'check_expose_information',
+  assign_hook => 'assign_expose_information',
+},
+
 { name => 'extension_control_path', type => 'string', context => 'PGC_SUSET', group => 'CLIENT_CONN_OTHER',
   short_desc => 'Sets the path for extension control files.',
   long_desc => 'The remaining extension script and secondary control files are then loaded from the same directory where the primary control file was found.',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index e759f06b50f..2de210567f3 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -93,6 +93,13 @@
                                         # disconnection while running queries;
                                         # 0 for never
 
+# - Expose information -
+
+#expose_information = '' # comma-separated list of items to expose
+                         # role = if the server is in recovery or not
+                         # sysid = the current system identifier for this server
+                         # version = the current version of this server
+
 # - Authentication -
 
 #authentication_timeout = 1min          # 1s-600s
diff --git a/src/include/tcop/backend_startup.h b/src/include/tcop/backend_startup.h
index d486f926319..feedc1f8a4e 100644
--- a/src/include/tcop/backend_startup.h
+++ b/src/include/tcop/backend_startup.h
@@ -20,6 +20,8 @@
 extern PGDLLIMPORT bool Trace_connection_negotiation;
 extern PGDLLIMPORT uint32 log_connections;
 extern PGDLLIMPORT char *log_connections_string;
+extern PGDLLIMPORT uint32 expose_information;
+extern PGDLLIMPORT char *expose_information_string;
 
 /* Other globals */
 extern PGDLLIMPORT struct ConnectionTiming conn_timing;
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 6a76f8d5ed6..40d31c9fe16 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -62,6 +62,8 @@ extern void assign_default_text_search_config(const char *newval, void *extra);
 extern bool check_default_with_oids(bool *newval, void **extra,
 									GucSource source);
 extern const char *show_effective_wal_level(void);
+extern bool check_expose_information(char **newval, void **extra, GucSource source);
+extern void assign_expose_information(const char *newval, void *extra);
 extern bool check_huge_page_size(int *newval, void **extra, GucSource source);
 extern void assign_io_method(int newval, void *extra);
 extern bool check_io_max_concurrency(int *newval, void **extra, GucSource source);
diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build
index 5d81f5b13be..7e9aeb0e804 100644
--- a/src/test/modules/test_misc/meson.build
+++ b/src/test/modules/test_misc/meson.build
@@ -24,6 +24,7 @@ tests += {
       't/013_temp_obj_multisession.pl',
       't/014_log_statement_max_length.pl',
       't/015_temp_schema_exit_deferrable.pl',
+      't/016_expose_information.pl',
     ],
     # The injection points are cluster-wide, so disable installcheck
     'runningcheck': false,
diff --git a/src/test/modules/test_misc/t/016_expose_information.pl b/src/test/modules/test_misc/t/016_expose_information.pl
new file mode 100644
index 00000000000..07d3b435c3c
--- /dev/null
+++ b/src/test/modules/test_misc/t/016_expose_information.pl
@@ -0,0 +1,179 @@
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# Test gathering information before authentication via expose_* variables
+
+# Force use of TCP/IP sockets via raw_connect
+INIT{ $PostgreSQL::Test::Utils::use_unix_sockets = 0; }
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+plan tests => 31;
+
+my $node = PostgreSQL::Test::Cluster->new('node1');
+
+# Set as logical here so we can restart it as a replica later
+$node->init(allows_streaming => 'logical');
+$node->start;
+
+my $server_version = $node->safe_psql('postgres', 'show server_version_num');
+my $bindir = $node->config_data('--bindir');
+my $datadir = $node->data_dir;
+my $cdata = qx{$bindir/pg_controldata -D $datadir 2>&1};
+my ($sysid) = $cdata =~ /Database system identifier:\s+(\d+)/;
+my $receive_length = 200;
+
+my ($socket, $response, $test);
+
+sub call_socket {
+	my $string = shift;
+	$socket->close() if defined $socket;
+	$socket = $node->raw_connect();
+	$socket->timeout(1);
+	$socket->send($string);
+	$response = '';
+	select(undef, undef, undef, 0.1);
+	$socket->recv($response, $receive_length);
+	return;
+}
+
+## Technically, this 'returns nothing' is a protocol error and a closed connection.
+## But for purposes of this test, we just need to make sure nothing is returned.
+
+$test = q{GET /foobar returns nothing when expose_information = ''};
+call_socket('GET /foobar');
+is ($response, '', $test);
+
+$test = q{HEAD /foobar returns nothing when expose_information = ''};
+call_socket('HEAD /foobar');
+is ($response, '', $test);
+
+$test = q{GET /replica returns nothing when expose_information=''};
+call_socket('GET /replica');
+is ($response, '', $test);
+
+$test = q{HEAD /replica returns nothing when expose_information=''};
+call_socket('HEAD /replica');
+is ($response, '', $test);
+
+$test = q{GET /primary returns nothing when expose_information=''};
+call_socket('GET /primary');
+is ($response, '', $test);
+
+$test = q{HEAD /primary returns nothing when expose_information=''};
+call_socket('HEAD /primary');
+is ($response, '', $test);
+
+$node->append_conf('postgresql.conf', "expose_information = 'role'");
+$node->reload();
+
+$test = q{GET /replica returns HTTP code 200 when expose_information = 'role' (primary)};
+call_socket('GET /replica');
+like ($response, qr{^HTTP/1.1 200 }, $test);
+
+$test = q{GET /replica returns string "0" when expose_information = 'role' (primary)};
+like ($response, qr{\r\n0\r\n}, $test);
+
+$test = q{HEAD /replica returns HTTP code 503 when expose_information = 'role' (primary)};
+call_socket('HEAD /replica');
+like ($response, qr{^HTTP/1.1 503 }, $test);
+
+$test = q{GET /primary returns HTTP code 200 when expose_information = 'role' (primary)};
+call_socket('GET /primary');
+like ($response, qr{^HTTP/1.1 200 }, $test);
+
+$test = q{GET /primary returns string "1" when expose_information = 'role' (primary)};
+like ($response, qr{\r\n1\r\n}, $test);
+
+$test = q{HEAD /primary returns HTTP code 200 when expose_information = 'role' (primary)};
+call_socket('HEAD /primary');
+like ($response, qr{^HTTP/1.1 200 }, $test);
+
+$test = q{GET /version returns nothing when expose_information = 'role'};
+call_socket('GET /version');
+is ($response, '', $test);
+
+$test = q{GET /sysid returns nothing when expose_information = 'role'};
+call_socket('GET /sysid');
+is ($response, '', $test);
+
+$node->append_conf('postgresql.conf', "expose_information= 'sysid'");
+$node->reload();
+
+$test = q{GET /replica returns nothing when expose_information = 'sysid'};
+call_socket('GET /replica');
+is ($response, '', $test);
+
+$test = q{HEAD /replica returns nothing when expose_information = 'sysid'};
+call_socket('HEAD /replica');
+is ($response, '', $test);
+
+$node->append_conf('postgresql.conf', "expose_information= 'sysid,role,version'");
+$node->reload();
+
+$test = q{GET /sysid returns correct value when expose_information contains 'sysid'};
+call_socket('GET /sysid');
+like ($response, qr/^$sysid\r\n/m, $test);
+
+$test = q{GET /version returns correct value when expose_information contains 'version'};
+call_socket('GET /version');
+like ($response, qr/^$server_version\r\n/m, $test);
+
+$test = q{GET /version\r\n returns correct value when expose_information contains 'version'};
+call_socket("GET /version\r\n");
+like ($response, qr/^$server_version\r\n/m, $test);
+
+$test = q{GET /version HTTP/1.0 returns correct value when expose_information contains 'version'};
+call_socket("GET /version HTTP/1.0");
+like ($response, qr/^$server_version\r\n/m, $test);
+
+$test = q{GET /ve returns nothing};
+call_socket('GET /ve');
+is ($response, '', $test);
+
+$test = q{GET /versio returns nothing};
+call_socket('GET /versio');
+is ($response, '', $test);
+
+$test = q{GET /versionx returns nothing};
+call_socket('GET /versionx');
+is ($response, '', $test);
+
+$node->set_standby_mode();
+$node->restart();
+
+$test = q{GET /replica returns HTTP code 200 when expose_information contains 'role' (replica)};
+call_socket('GET /replica');
+like ($response, qr{^HTTP/1.1 200 }, $test);
+
+$test = q{GET /replica returns string "1" when expose_information contains 'role' (replica)};
+like ($response, qr{^1\r\n}m, $test);
+
+$test = q{HEAD /replica returns HTTP code 200 when expose_information contains 'role' (replica)};
+call_socket('HEAD /replica');
+like ($response, qr{^HTTP/1.1 200 }, $test);
+
+$test = q{GET /primary returns HTTP code 200 when expose_information contains 'role' (replica)};
+call_socket('GET /primary');
+like ($response, qr{^HTTP/1.1 200 }, $test);
+
+$test = q{GET /primary returns string "0" when expose_information contains 'role' (replica)};
+like ($response, qr{^0\r\n}m, $test);
+
+$test = q{HEAD /primary returns HTTP code 503 when expose_information contains 'role' (replica)};
+call_socket('HEAD /primary');
+like ($response, qr{^HTTP/1.1 503 }, $test);
+
+$test = q{Regular connection still works after expose_information is enabled};
+is ($node->safe_psql('postgres', 'select 42'), '42', $test);
+
+$node->append_conf('postgresql.conf', "expose_information=''");
+$node->reload();
+
+$test = q{GET /version returns nothing after expose_information no longer has 'version'};
+call_socket('GET /version');
+is ($response, '', $test);
+
+$socket->close();
-- 
2.47.3

