Compare commits

...
5 Commits
Author SHA1 Message Date
steve-chavez 272e2e7535 bump version to 13.0.6 2025-08-30 13:55:34 -05:00
Laurence IslaandWolfgang Walther ea153523d1 fix: empty enum in 'preferParams' openAPI parameter 2025-08-30 08:31:21 +02:00
Taimoor ZaeemandWolfgang Walther bf0a1173b5 fix: format of IPv6 address logged at PostgREST startup
The IPv6 address logged at the startup like `::1:80` was
wrong because the port isn't clearly separated. This commit
corrects it, now logging as `[::1]:80`.

This is done in accordance to RFC 3986. In short, we did this
have a clear separation between the port and host because
the components of an IPv6 are separated with the ':' character.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-08-29 22:31:01 +02:00
renovate[bot]andWolfgang Walther 3a28968f3c chore(deps): update nixbuild/nix-quick-install-action action to v33 2025-08-25 15:08:25 +00:00
Laurence IslaandWolfgang Walther 86aac1ead5 fix: logging the Haskell type instead of the listener error message directly
Previously: Just "connection error..."
Now: connection error...
2025-08-25 10:25:48 +00:00
9 changed files with 71 additions and 19 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ inputs:
runs: runs:
using: composite using: composite
steps: steps:
- uses: nixbuild/nix-quick-install-action@63ca48f939ee3b8d835f4126562537df0fee5b91 # v32 - uses: nixbuild/nix-quick-install-action@1f095fee853b33114486cfdeae62fa099cda35a9 # v33
with: with:
nix_conf: |- nix_conf: |-
always-allow-substitutes = true always-allow-substitutes = true
+8
View File
@@ -5,6 +5,14 @@ This project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased ## Unreleased
## [13.0.6] - 2025-08-30
### Fixed
- Fix logging the Haskell type instead of the listener error message directly by @laurenceisla in #3588
- Fix format of `IPv6` address logged at PostgREST startup by @taimoorzaeem in #4291
- Fix empty enum in `preferParams` OpenAPI parameter by @laurenceisla in #4292
## [13.0.5] - 2025-08-24 ## [13.0.5] - 2025-08-24
### Fixed ### Fixed
+1 -1
View File
@@ -1,5 +1,5 @@
name: postgrest name: postgrest
version: 13.0.5 version: 13.0.6
synopsis: REST API for any Postgres database synopsis: REST API for any Postgres database
description: Reads the schema of a PostgreSQL database and creates RESTful routes description: Reads the schema of a PostgreSQL database and creates RESTful routes
for tables, views, and functions, supporting all HTTP methods that security for tables, views, and functions, supporting all HTTP methods that security
+5 -1
View File
@@ -13,5 +13,9 @@ resolveHost sock = do
sn <- NS.getSocketName sock sn <- NS.getSocketName sock
case sn of case sn of
NS.SockAddrInet _ hostAddr -> pure $ Just $ fromString $ show $ fromHostAddress hostAddr NS.SockAddrInet _ hostAddr -> pure $ Just $ fromString $ show $ fromHostAddress hostAddr
NS.SockAddrInet6 _ _ hostAddr6 _ -> pure $ Just $ fromString $ show $ fromHostAddress6 hostAddr6 -- The IPv6 addresses are wrapped in [] brackets. This is done in accordance
-- to RFC 3986 (https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2).
-- In short, we did this to have a clear separation between the port and host
-- because the components of an IPv6 are separated with the ':' character.
NS.SockAddrInet6 _ _ hostAddr6 _ -> pure $ Just $ fromString $ "[" ++ show (fromHostAddress6 hostAddr6) ++ "]"
_ -> pure Nothing _ -> pure Nothing
+10 -10
View File
@@ -108,11 +108,8 @@ observationMessage = \case
DBListenStart channel -> do DBListenStart channel -> do
"Listening for database notifications on the " <> show channel <> " channel" "Listening for database notifications on the " <> show channel <> " channel"
DBListenFail channel listenErr -> DBListenFail channel listenErr ->
"Failed listening for database notifications on the " <> show channel <> " channel. " <> ( "Failed listening for database notifications on the " <> show channel <> " channel. " <>
case listenErr of either showListenerConnError showListenerException listenErr
Left err -> show err
Right err -> showListenerError err
)
DBListenRetry delay -> DBListenRetry delay ->
"Retrying listening for database notifications in " <> (show delay::Text) <> " seconds..." "Retrying listening for database notifications in " <> (show delay::Text) <> " seconds..."
DBListenerGotSCacheMsg channel -> DBListenerGotSCacheMsg channel ->
@@ -157,8 +154,11 @@ observationMessage = \case
jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.PgError False err jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.PgError False err
showListenerError :: Either SomeException () -> Text showOnSingleLine txt = T.intercalate " " $ T.filter (/= '\t') <$> T.lines txt -- the errors from hasql-notifications come intercalated with "\t\n"
showListenerError (Right _) = "Failed getting notifications" -- should not happen as the listener will never finish (hasql-notifications uses `forever` internally) with a Right result
showListenerError (Left e) = showListenerConnError :: SQL.ConnectionError -> Text
let showOnSingleLine txt = T.intercalate " " $ T.filter (/= '\t') <$> T.lines txt in -- the errors from hasql-notifications come intercalated with "\t\n" showListenerConnError = maybe "Connection error" (showOnSingleLine . T.decodeUtf8)
showOnSingleLine $ show e
showListenerException :: Either SomeException () -> Text
showListenerException (Right _) = "Failed getting notifications" -- should not happen as the listener will never finish (hasql-notifications uses `forever` internally) with a Right result
showListenerException (Left e) = showOnSingleLine $ show e
+2 -1
View File
@@ -171,8 +171,9 @@ makePreferParam ts =
& schema .~ ParamOther ((mempty :: ParamOtherSchema) & schema .~ ParamOther ((mempty :: ParamOtherSchema)
& in_ .~ ParamHeader & in_ .~ ParamHeader
& type_ ?~ SwaggerString & type_ ?~ SwaggerString
& enum_ .~ JSON.decode (JSON.encode $ foldl (<>) [] (val <$> ts))) & enum_ .~ if null enu then Nothing else JSON.decode (JSON.encode enu))
where where
enu = foldl (<>) [] (val <$> ts)
val :: Text -> [Text] val :: Text -> [Text]
val = \case val = \case
"count" -> ["count=none"] "count" -> ["count=none"]
+13 -2
View File
@@ -96,7 +96,9 @@ def run(
if port: if port:
env["PGRST_SERVER_PORT"] = str(port) env["PGRST_SERVER_PORT"] = str(port)
env["PGRST_SERVER_HOST"] = host or "localhost" env["PGRST_SERVER_HOST"] = host or "localhost"
baseurl = f"http://localhost:{port}" # When constructing IPv6 address, host address should be bracketed like [host]
apihost = f"[{host}]" if host and is_ipv6(host) else "localhost"
baseurl = f"http://{apihost}:{port}"
else: else:
socketfile = pathlib.Path(tmpdir) / "postgrest.sock" socketfile = pathlib.Path(tmpdir) / "postgrest.sock"
env["PGRST_SERVER_UNIX_SOCKET"] = str(socketfile) env["PGRST_SERVER_UNIX_SOCKET"] = str(socketfile)
@@ -104,7 +106,8 @@ def run(
adminport = freeport(port) adminport = freeport(port)
env["PGRST_ADMIN_SERVER_PORT"] = str(adminport) env["PGRST_ADMIN_SERVER_PORT"] = str(adminport)
adminurl = f"http://localhost:{adminport}" adminhost = f"[{host}]" if host and is_ipv6(host) else "localhost"
adminurl = f"http://{adminhost}:{adminport}"
command = [POSTGREST_BIN] command = [POSTGREST_BIN]
env["HPCTIXFILE"] = hpctixfile() env["HPCTIXFILE"] = hpctixfile()
@@ -218,3 +221,11 @@ def sleep_pool_connection(url, seconds):
session.get(url + f"/rpc/sleep?seconds={seconds}", timeout=0.1) session.get(url + f"/rpc/sleep?seconds={seconds}", timeout=0.1)
except requests.exceptions.ReadTimeout: except requests.exceptions.ReadTimeout:
pass pass
def is_ipv6(addr):
try:
socket.inet_pton(socket.AF_INET6, addr)
return True
except OSError:
return False
+24 -3
View File
@@ -1355,9 +1355,9 @@ def test_log_postgrest_version(defaultenv):
assert "Starting PostgREST %s..." % version in output[0] assert "Starting PostgREST %s..." % version in output[0]
def test_log_postgrest_host_and_port(defaultenv): @pytest.mark.parametrize("host", ["127.0.0.1", "::1"])
def test_log_postgrest_host_and_port(host, defaultenv):
"PostgREST should output the host and port it is bound to." "PostgREST should output the host and port it is bound to."
host = "127.0.0.1"
port = freeport() port = freeport()
with run( with run(
@@ -1365,7 +1365,10 @@ def test_log_postgrest_host_and_port(defaultenv):
) as postgrest: ) as postgrest:
output = postgrest.read_stdout(nlines=10) output = postgrest.read_stdout(nlines=10)
assert f"API server listening on {host}:{port}" in output[2] # output-sensitive if is_ipv6(host): # IPv6
assert f"API server listening on [{host}]:{port}" in output[2]
else: # IPv4
assert f"API server listening on {host}:{port}" in output[2]
def test_succeed_w_role_having_superuser_settings(defaultenv): def test_succeed_w_role_having_superuser_settings(defaultenv):
@@ -1930,3 +1933,21 @@ def test_schema_cache_error_observation(defaultenv):
"Failed to load the schema cache using db-schemas=public and db-extra-search-path=x" "Failed to load the schema cache using db-schemas=public and db-extra-search-path=x"
in output[7] in output[7]
) )
def test_log_listener_connection_errors(defaultenv):
"The logs should show the listener connection error message in a single line"
env = {
**defaultenv,
"PGHOST": "no_host",
"PGRST_DB_CHANNEL_ENABLED": "true",
}
with run(env=env, no_startup_stdout=False, wait_for_readiness=False) as postgrest:
output = postgrest.read_stdout(nlines=5)
assert any(
'Failed listening for database notifications on the "pgrst" channel. could not translate host name "no_host" to address:'
in line
for line in output
)
+7
View File
@@ -1085,6 +1085,13 @@ spec = describe "OpenAPI" $ do
immutableGet `shouldNotBe` Nothing immutableGet `shouldNotBe` Nothing
immutablePost `shouldNotBe` Nothing immutablePost `shouldNotBe` Nothing
it "does not include empty enum in the preferParams parameter" $ do
r <- simpleBody <$> get "/"
let preferParams = r ^? key "parameters" . key "preferParams" . key "enum"
liftIO $ do
preferParams `shouldBe` Nothing
describe "Security" $ describe "Security" $
it "does not include security or security definitions by default" $ do it "does not include security or security definitions by default" $ do
r <- simpleBody <$> get "/" r <- simpleBody <$> get "/"