add: config client-error-verbosity to set error verbosity

Set error verbosity using this config. The verbosity can
be set to `verbose` or `minimal` for client error responses.

This only affects client side HTTP responses, server side logs
are not affected by this config.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
This commit is contained in:
Taimoor Zaeem
2026-02-25 15:24:27 -05:00
committed by Steve Chavez
parent 2edc44c352
commit 83dc082acf
29 changed files with 179 additions and 38 deletions
+1
View File
@@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file. From versio
- Log host, port and pg version of listener database connection by @mkleczek in #4617 #4618 - Log host, port and pg version of listener database connection by @mkleczek in #4617 #4618
- Optimize requests with `Prefer: count=exact` that do not use ranges or `db-max-rows` by @laurenceisla in #3957 - Optimize requests with `Prefer: count=exact` that do not use ranges or `db-max-rows` by @laurenceisla in #3957
+ Removed unnecessary double count when building the `Content-Range`. + Removed unnecessary double count when building the `Content-Range`.
- Add config `client_error_verbosity` to customize error verbosity by @taimoorzaeem in #4088
### Changed ### Changed
+47
View File
@@ -195,6 +195,53 @@ app.settings.*
The :code:`current_setting` function has `an optional boolean second <https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADMIN-SET>`_ argument to avoid it from raising an error if the value was not defined. Default values to :code:`app.settings` can then be given by combining this argument with :code:`coalesce` and :code:`nullif` : :code:`coalesce(nullif(current_setting('app.settings.my_custom_variable', true), ''), 'default value')`. The use of :code:`nullif` is necessary because if set in a transaction, the setting is sometimes not "rolled back" to :code:`null`. See also :ref:`this section <guc_req_headers_cookies_claims>` for more information on this behaviour. The :code:`current_setting` function has `an optional boolean second <https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADMIN-SET>`_ argument to avoid it from raising an error if the value was not defined. Default values to :code:`app.settings` can then be given by combining this argument with :code:`coalesce` and :code:`nullif` : :code:`coalesce(nullif(current_setting('app.settings.my_custom_variable', true), ''), 'default value')`. The use of :code:`nullif` is necessary because if set in a transaction, the setting is sometimes not "rolled back" to :code:`null`. See also :ref:`this section <guc_req_headers_cookies_claims>` for more information on this behaviour.
.. _client-error-verbosity:
client-error-verbosity
----------------------
=============== =======================
**Type** String
**Default** verbose
**Reloadable** Y
**Environment** PGRST_CLIENT_ERROR_VERBOSITY
**In-Database** pgrst.client_error_verbosity
=============== =======================
Specifies the verbosity of PostgREST errors.
With ``verbose``, ``code``, ``message``, ``details`` and ``hint`` are returned.
.. code:: bash
curl "localhost:3000/itemsxx"
.. code-block:: json
{
"code": "PGRST205",
"message": "Could not find the table 'public.itemsxx' in the schema cache",
"details": "Perhaps you meant the table 'public.items'",
"hint": null
}
With ``minimal``, just ``code`` and ``message`` are returned.
.. code:: bash
curl "localhost:3000/itemsxx"
.. code-block:: json
{
"code": "PGRST205",
"message": "Could not find the table 'public.itemsxx' in the schema cache"
}
.. note::
This setting only affects client side error messages. Server side logs are not affected by this setting.
.. _db-aggregates-enabled: .. _db-aggregates-enabled:
db-aggregates-enabled db-aggregates-enabled
+23 -22
View File
@@ -119,30 +119,31 @@ postgrest logLevel appState connWorker =
Logger.middleware logLevel Auth.getRole $ Logger.middleware logLevel Auth.getRole $
-- fromJust can be used, because the auth middleware will **always** add -- fromJust can be used, because the auth middleware will **always** add
-- some AuthResult to the vault. -- some AuthResult to the vault.
\req respond -> case fromJust $ Auth.getResult req of \req respond -> do
Left err -> respond $ Error.errorResponseFor err appConf@AppConfig{..} <- AppState.getConfig appState -- the config must be read again because it can reload
Right authResult -> do case fromJust $ Auth.getResult req of
appConf <- AppState.getConfig appState -- the config must be read again because it can reload Left err -> respond $ Error.errorResponseFor configClientErrorVerbosity err
maybeSchemaCache <- AppState.getSchemaCache appState Right authResult -> do
maybeSchemaCache <- AppState.getSchemaCache appState
let let
eitherResponse :: IO (Either Error Wai.Response) eitherResponse :: IO (Either Error Wai.Response)
eitherResponse = eitherResponse =
runExceptT $ postgrestResponse appState appConf maybeSchemaCache authResult req runExceptT $ postgrestResponse appState appConf maybeSchemaCache authResult req
response <- either Error.errorResponseFor identity <$> eitherResponse response <- either (Error.errorResponseFor configClientErrorVerbosity) identity <$> eitherResponse
-- Launch the connWorker when the connection is down. The postgrest -- Launch the connWorker when the connection is down. The postgrest
-- function can respond successfully (with a stale schema cache) before -- function can respond successfully (with a stale schema cache) before
-- the connWorker is done. However, when there's an empty schema cache -- the connWorker is done. However, when there's an empty schema cache
-- postgrest responds with the error `PGRST002`; this means that the schema -- postgrest responds with the error `PGRST002`; this means that the schema
-- cache is still loading, so we don't launch the connWorker here because -- cache is still loading, so we don't launch the connWorker here because
-- it would duplicate the loading process, e.g. https://github.com/PostgREST/postgrest/issues/3704 -- it would duplicate the loading process, e.g. https://github.com/PostgREST/postgrest/issues/3704
-- TODO: this process may be unnecessary when the Listener is enabled. Revisit once https://github.com/PostgREST/postgrest/issues/1766 is done -- TODO: this process may be unnecessary when the Listener is enabled. Revisit once https://github.com/PostgREST/postgrest/issues/1766 is done
when (isServiceUnavailable response && isJust maybeSchemaCache) connWorker when (isServiceUnavailable response && isJust maybeSchemaCache) connWorker
resp <- do resp <- do
delay <- AppState.getNextDelay appState delay <- AppState.getNextDelay appState
return $ addRetryHint delay response return $ addRetryHint delay response
respond resp respond resp
postgrestResponse postgrestResponse
:: AppState.AppState :: AppState.AppState
+25 -1
View File
@@ -29,6 +29,7 @@ module PostgREST.Config
, addTargetSessionAttrs , addTargetSessionAttrs
, exampleConfigFile , exampleConfigFile
, audMatchesCfg , audMatchesCfg
, Verbosity (..)
) where ) where
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
@@ -73,6 +74,7 @@ audMatchesCfg = maybe (const True) (==) . configJwtAudience
data AppConfig = AppConfig data AppConfig = AppConfig
{ configAppSettings :: [(Text, Text)] { configAppSettings :: [(Text, Text)]
, configClientErrorVerbosity :: Verbosity
, configDbAggregates :: Bool , configDbAggregates :: Bool
, configDbAnonRole :: Maybe BS.ByteString , configDbAnonRole :: Maybe BS.ByteString
, configDbChannel :: Text , configDbChannel :: Text
@@ -134,6 +136,15 @@ dumpLogLevel = \case
LogInfo -> "info" LogInfo -> "info"
LogDebug -> "debug" LogDebug -> "debug"
data Verbosity
= Minimal
| Verbose
dumpClientErrorVerbosity :: Verbosity -> Text
dumpClientErrorVerbosity = \case
Minimal -> "minimal"
Verbose -> "verbose"
data OpenAPIMode = OAFollowPriv | OAIgnorePriv | OADisabled data OpenAPIMode = OAFollowPriv | OAIgnorePriv | OADisabled
deriving Eq deriving Eq
@@ -150,7 +161,8 @@ toText conf =
where where
-- apply conf to all pgrst settings -- apply conf to all pgrst settings
pgrstSettings = (\(k, v) -> (k, v conf)) <$> pgrstSettings = (\(k, v) -> (k, v conf)) <$>
[("db-aggregates-enabled", T.toLower . show . configDbAggregates) [("client-error-verbosity", q . dumpClientErrorVerbosity . configClientErrorVerbosity)
,("db-aggregates-enabled", T.toLower . show . configDbAggregates)
,("db-anon-role", q . T.decodeUtf8 . fromMaybe "" . configDbAnonRole) ,("db-anon-role", q . T.decodeUtf8 . fromMaybe "" . configDbAnonRole)
,("db-channel", q . configDbChannel) ,("db-channel", q . configDbChannel)
,("db-channel-enabled", T.toLower . show . configDbChannelEnabled) ,("db-channel-enabled", T.toLower . show . configDbChannelEnabled)
@@ -254,6 +266,7 @@ parser :: Maybe FilePath -> Environment -> [(Text, Text)] -> RoleSettings -> Rol
parser optPath env dbSettings roleSettings roleIsolationLvl = parser optPath env dbSettings roleSettings roleIsolationLvl =
AppConfig AppConfig
<$> parseAppSettings "app.settings" <$> parseAppSettings "app.settings"
<*> parseErrorVerbosity "client-error-verbosity"
<*> (fromMaybe False <$> optBool "db-aggregates-enabled") <*> (fromMaybe False <$> optBool "db-aggregates-enabled")
<*> (fmap encodeUtf8 <$> optString "db-anon-role") <*> (fmap encodeUtf8 <$> optString "db-anon-role")
<*> (fromMaybe "pgrst" <$> optString "db-channel") <*> (fromMaybe "pgrst" <$> optString "db-channel")
@@ -310,6 +323,14 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
<*> optInt "internal-schema-cache-load-sleep" <*> optInt "internal-schema-cache-load-sleep"
<*> optInt "internal-schema-cache-relationship-load-sleep" <*> optInt "internal-schema-cache-relationship-load-sleep"
where where
parseErrorVerbosity :: C.Key -> C.Parser C.Config Verbosity
parseErrorVerbosity k =
optString k >>= \case
Nothing -> pure Verbose -- default
Just "minimal" -> pure Minimal
Just "verbose" -> pure Verbose
Just _ -> fail "Invalid client-error-verbosity. Check your configuration."
parseAppSettings :: C.Key -> C.Parser C.Config [(Text, Text)] parseAppSettings :: C.Key -> C.Parser C.Config [(Text, Text)]
parseAppSettings key = addFromEnv . fmap (fmap coerceText) <$> C.subassocs key C.value parseAppSettings key = addFromEnv . fmap (fmap coerceText) <$> C.subassocs key C.value
where where
@@ -642,6 +663,9 @@ exampleConfigFile = S.unlines
[ "## Admin server used for checks. It's disabled by default unless a port is specified." [ "## Admin server used for checks. It's disabled by default unless a port is specified."
, "# admin-server-port = 3001" , "# admin-server-port = 3001"
, "" , ""
, "# PostgREST error json verbosity config"
, "# client-error-verbosity = \"verbose\""
, ""
, "## The database role to use when no client authentication is provided" , "## The database role to use when no client authentication is provided"
, "# db-anon-role = \"anon\"" , "# db-anon-role = \"anon\""
, "" , ""
+1
View File
@@ -46,6 +46,7 @@ dbSettingsNames :: [Text]
dbSettingsNames = dbSettingsNames =
(prefix <>) <$> (prefix <>) <$>
["db_aggregates_enabled" ["db_aggregates_enabled"
,"client_error_verbosity"
,"db_anon_role" ,"db_anon_role"
,"db_pre_config" ,"db_pre_config"
,"db_extra_search_path" ,"db_extra_search_path"
+12 -7
View File
@@ -42,6 +42,7 @@ import Network.HTTP.Types.Header (Header)
import PostgREST.MediaType (MediaType (..)) import PostgREST.MediaType (MediaType (..))
import qualified PostgREST.MediaType as MediaType import qualified PostgREST.MediaType as MediaType
import PostgREST.Config (Verbosity (..))
import PostgREST.SchemaCache (SchemaCache (SchemaCache, dbTablesFuzzyIndex)) import PostgREST.SchemaCache (SchemaCache (SchemaCache, dbTablesFuzzyIndex))
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..), import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
Schema) Schema)
@@ -57,26 +58,30 @@ import PostgREST.Error.Types
import Protolude import Protolude
-- | Encode Error to ByteString -- | Encode Error to ByteString
errorPayload :: (ErrorBody a, ErrorHeaders a) => a -> LByteString errorPayload :: (ErrorBody a, ErrorHeaders a) => Verbosity -> a -> LByteString
errorPayload = JSON.encode . toJsonPgrstError errorPayload verb = JSON.encode . toJsonPgrstError verb
where where
toJsonPgrstError :: (ErrorBody a, ErrorHeaders a) => a -> JSON.Value toJsonPgrstError :: (ErrorBody a, ErrorHeaders a) => Verbosity -> a -> JSON.Value
toJsonPgrstError err = JSON.object [ toJsonPgrstError Verbose err = JSON.object [
"code" .= code err "code" .= code err
, "message" .= message err , "message" .= message err
, "details" .= details err , "details" .= details err
, "hint" .= hint err , "hint" .= hint err
] ]
toJsonPgrstError Minimal err = JSON.object [
"code" .= code err
, "message" .= message err
]
-- | Create HTTP response from Error -- | Create HTTP response from Error
errorResponseFor :: (ErrorBody a, ErrorHeaders a) => a -> Response errorResponseFor :: (ErrorBody a, ErrorHeaders a) => Verbosity -> a -> Response
errorResponseFor err = errorResponseFor verb err =
let let
baseHeader = MediaType.toContentType MTApplicationJSON baseHeader = MediaType.toContentType MTApplicationJSON
cLHeader body = (,) "Content-Length" (show $ LBS.length body) :: Header cLHeader body = (,) "Content-Length" (show $ LBS.length body) :: Header
pSHeader code' = ("Proxy-Status", "PostgREST; error=" <> T.encodeUtf8 code') pSHeader code' = ("Proxy-Status", "PostgREST; error=" <> T.encodeUtf8 code')
in in
responseLBS (status err) (baseHeader : cLHeader (errorPayload err) : pSHeader (code err) : headers err) $ errorPayload err responseLBS (status err) (baseHeader : cLHeader (errorPayload verb err) : pSHeader (code err) : headers err) $ errorPayload verb err
class ErrorHeaders a where class ErrorHeaders a where
status :: a -> HTTP.Status status :: a -> HTTP.Status
-1
View File
@@ -25,7 +25,6 @@ import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
import PostgREST.SchemaCache.Relationship (Relationship (..), import PostgREST.SchemaCache.Relationship (Relationship (..),
RelationshipsMap) RelationshipsMap)
import PostgREST.SchemaCache.Routine (Routine (..)) import PostgREST.SchemaCache.Routine (Routine (..))
import Protolude import Protolude
data Error data Error
+3 -2
View File
@@ -24,6 +24,7 @@ import qualified Hasql.Pool as SQL
import qualified Hasql.Pool.Observation as SQL import qualified Hasql.Pool.Observation as SQL
import Network.HTTP.Types.Status (Status) import Network.HTTP.Types.Status (Status)
import Numeric (showFFloat) import Numeric (showFFloat)
import PostgREST.Config (Verbosity (..))
import PostgREST.Config.PgVersion import PostgREST.Config.PgVersion
import qualified PostgREST.Error as Error import qualified PostgREST.Error as Error
import PostgREST.Query (MainQuery) import PostgREST.Query (MainQuery)
@@ -94,7 +95,7 @@ observationMessage = \case
ExitDBFatalError ServerError08P01 usageErr -> ExitDBFatalError ServerError08P01 usageErr ->
"Connection poolers in statement mode are not supported." <> jsonMessage usageErr "Connection poolers in statement mode are not supported." <> jsonMessage usageErr
SchemaCacheEmptyObs -> SchemaCacheEmptyObs ->
T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.NoSchemaCacheError T.decodeUtf8 . LBS.toStrict . Error.errorPayload Verbose $ Error.NoSchemaCacheError
SchemaCacheErrorObs dbSchemas extraPaths usageErr -> SchemaCacheErrorObs dbSchemas extraPaths usageErr ->
"Failed to load the schema cache using " "Failed to load the schema cache using "
<> "db-schemas=" <> T.intercalate "," (toList dbSchemas) <> "db-schemas=" <> T.intercalate "," (toList dbSchemas)
@@ -167,7 +168,7 @@ observationMessage = \case
showMillis :: Double -> Text showMillis :: Double -> Text
showMillis x = toS $ showFFloat (Just 1) x "" showMillis x = toS $ showFFloat (Just 1) x ""
jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.PgError False err jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload Verbose $ Error.PgError False err
showListenerConnError :: SQL.ConnectionError -> Text showListenerConnError :: SQL.ConnectionError -> Text
+4 -4
View File
@@ -62,7 +62,7 @@ data PgrstResponse = PgrstResponse {
actionResponse :: DbResult -> ApiRequest -> (Text, Text) -> AppConfig -> SchemaCache -> Schema -> Bool -> Either Error.Error PgrstResponse actionResponse :: DbResult -> ApiRequest -> (Text, Text) -> AppConfig -> SchemaCache -> Schema -> Bool -> Either Error.Error PgrstResponse
actionResponse (DbCrudResult plan@WrappedReadPlan{pMedia, wrHdrsOnly=headersOnly, crudQi=identifier} RSStandard{..}) ctxApiRequest@ApiRequest{..} _ _ _ _ _ = do actionResponse (DbCrudResult plan@WrappedReadPlan{pMedia, wrHdrsOnly=headersOnly, crudQi=identifier} RSStandard{..}) ctxApiRequest@ApiRequest{..} _ AppConfig{..} _ _ _ = do
let let
(status, contentRange) = RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal (status, contentRange) = RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal
cLHeader = if headersOnly then mempty else [ contentLengthHeader bod ] cLHeader = if headersOnly then mempty else [ contentLengthHeader bod ]
@@ -79,7 +79,7 @@ actionResponse (DbCrudResult plan@WrappedReadPlan{pMedia, wrHdrsOnly=headersOnly
++ cLHeader ++ cLHeader
++ contentTypeHeaders pMedia ctxApiRequest ++ contentTypeHeaders pMedia ctxApiRequest
++ prefHeader ++ prefHeader
bod | status == HTTP.status416 = Error.errorPayload $ Error.ApiRequestErr $ Error.InvalidRange $ bod | status == HTTP.status416 = Error.errorPayload configClientErrorVerbosity $ Error.ApiRequestErr $ Error.InvalidRange $
Error.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal) Error.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal)
| headersOnly = mempty | headersOnly = mempty
| otherwise = LBS.fromStrict rsBody | otherwise = LBS.fromStrict rsBody
@@ -178,12 +178,12 @@ actionResponse (DbCrudResult plan@MutateReadPlan{mrMutation=MutationDelete, pMed
Right $ PgrstResponse ovStatus ovHeaders body Right $ PgrstResponse ovStatus ovHeaders body
actionResponse (DbCrudResult plan@CallReadPlan{pMedia, crInvMthd=invMethod, crProc=proc} RSStandard {..}) ctxApiRequest@ApiRequest{..} _ _ _ _ _ = do actionResponse (DbCrudResult plan@CallReadPlan{pMedia, crInvMthd=invMethod, crProc=proc} RSStandard {..}) ctxApiRequest@ApiRequest{..} _ AppConfig{..} _ _ _ = do
let let
(status, contentRange) = (status, contentRange) =
RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal
rsOrErrBody = if status == HTTP.status416 rsOrErrBody = if status == HTTP.status416
then Error.errorPayload $ Error.ApiRequestErr $ Error.InvalidRange then Error.errorPayload configClientErrorVerbosity $ Error.ApiRequestErr $ Error.InvalidRange
$ Error.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal) $ Error.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal)
else LBS.fromStrict rsBody else LBS.fromStrict rsBody
isHeadMethod = invMethod == InvRead True isHeadMethod = invMethod == InvRead True
+1
View File
@@ -1,3 +1,4 @@
client-error-verbosity = "verbose"
db-aggregates-enabled = false db-aggregates-enabled = false
db-anon-role = "" db-anon-role = ""
db-channel = "pgrst" db-channel = "pgrst"
@@ -1,3 +1,4 @@
client-error-verbosity = "verbose"
db-aggregates-enabled = false db-aggregates-enabled = false
db-anon-role = "" db-anon-role = ""
db-channel = "pgrst" db-channel = "pgrst"
@@ -1,3 +1,4 @@
client-error-verbosity = "verbose"
db-aggregates-enabled = false db-aggregates-enabled = false
db-anon-role = "" db-anon-role = ""
db-channel = "pgrst" db-channel = "pgrst"
+1
View File
@@ -1,3 +1,4 @@
client-error-verbosity = "verbose"
db-aggregates-enabled = false db-aggregates-enabled = false
db-anon-role = "" db-anon-role = ""
db-channel = "pgrst" db-channel = "pgrst"
@@ -1,3 +1,4 @@
client-error-verbosity = "verbose"
db-aggregates-enabled = false db-aggregates-enabled = false
db-anon-role = "" db-anon-role = ""
db-channel = "pgrst" db-channel = "pgrst"
@@ -1,3 +1,4 @@
client-error-verbosity = "verbose"
db-aggregates-enabled = false db-aggregates-enabled = false
db-anon-role = "" db-anon-role = ""
db-channel = "pgrst" db-channel = "pgrst"
@@ -1,3 +1,4 @@
client-error-verbosity = "verbose"
db-aggregates-enabled = false db-aggregates-enabled = false
db-anon-role = "" db-anon-role = ""
db-channel = "pgrst" db-channel = "pgrst"
@@ -1,3 +1,4 @@
client-error-verbosity = "verbose"
db-aggregates-enabled = false db-aggregates-enabled = false
db-anon-role = "" db-anon-role = ""
db-channel = "pgrst" db-channel = "pgrst"
@@ -1,3 +1,4 @@
client-error-verbosity = "verbose"
db-aggregates-enabled = false db-aggregates-enabled = false
db-anon-role = "" db-anon-role = ""
db-channel = "pgrst" db-channel = "pgrst"
@@ -1,3 +1,4 @@
client-error-verbosity = "minimal"
db-aggregates-enabled = false db-aggregates-enabled = false
db-anon-role = "pre_config_role" db-anon-role = "pre_config_role"
db-channel = "postgrest" db-channel = "postgrest"
@@ -1,3 +1,4 @@
client-error-verbosity = "minimal"
db-aggregates-enabled = false db-aggregates-enabled = false
db-anon-role = "anonymous" db-anon-role = "anonymous"
db-channel = "postgrest" db-channel = "postgrest"
@@ -1,3 +1,4 @@
client-error-verbosity = "minimal"
db-aggregates-enabled = true db-aggregates-enabled = true
db-anon-role = "root" db-anon-role = "root"
db-channel = "postgrest" db-channel = "postgrest"
+1
View File
@@ -1,3 +1,4 @@
client-error-verbosity = "verbose"
db-aggregates-enabled = false db-aggregates-enabled = false
db-anon-role = "" db-anon-role = ""
db-channel = "pgrst" db-channel = "pgrst"
+1
View File
@@ -1,3 +1,4 @@
client-error-verbosity = "verbose"
db-aggregates-enabled = false db-aggregates-enabled = false
db-anon-role = "" db-anon-role = ""
db-channel = "pgrst" db-channel = "pgrst"
+1
View File
@@ -1,5 +1,6 @@
PGRST_APP_SETTINGS_test2: test PGRST_APP_SETTINGS_test2: test
PGRST_APP_SETTINGS_test: test PGRST_APP_SETTINGS_test: test
PGRST_CLIENT_ERROR_VERBOSITY: minimal
PGRST_DB_AGGREGATES_ENABLED: true PGRST_DB_AGGREGATES_ENABLED: true
PGRST_DB_ANON_ROLE: root PGRST_DB_ANON_ROLE: root
PGRST_DB_CHANNEL: postgrest PGRST_DB_CHANNEL: postgrest
+1
View File
@@ -1,3 +1,4 @@
client-error-verbosity = "minimal"
db-aggregates-enabled = true db-aggregates-enabled = true
db-anon-role = "root" db-anon-role = "root"
db-channel = "postgrest" db-channel = "postgrest"
+2
View File
@@ -2,6 +2,7 @@ CREATE ROLE db_config_authenticator LOGIN NOINHERIT;
-- reloadable config options -- reloadable config options
-- these settings will override the values in configs/no-defaults.config, so they must be different -- these settings will override the values in configs/no-defaults.config, so they must be different
ALTER ROLE db_config_authenticator SET pgrst.client_error_verbosity = 'minimal';
ALTER ROLE db_config_authenticator SET pgrst.db_aggregates_enabled = 'false'; ALTER ROLE db_config_authenticator SET pgrst.db_aggregates_enabled = 'false';
ALTER ROLE db_config_authenticator SET pgrst.db_anon_role = 'anonymous'; ALTER ROLE db_config_authenticator SET pgrst.db_anon_role = 'anonymous';
ALTER ROLE db_config_authenticator SET pgrst.db_extra_search_path = 'public, extensions'; ALTER ROLE db_config_authenticator SET pgrst.db_extra_search_path = 'public, extensions';
@@ -56,6 +57,7 @@ ALTER ROLE db_config_authenticator SET pgrst.server_unix_socket_mode = 'ignored'
-- other authenticator reloadable config options -- other authenticator reloadable config options
-- these settings will override the values in configs/no-defaults.config, so they must be different -- these settings will override the values in configs/no-defaults.config, so they must be different
CREATE ROLE other_authenticator LOGIN NOINHERIT; CREATE ROLE other_authenticator LOGIN NOINHERIT;
ALTER ROLE other_authenticator SET pgrst.client_error_verbosity = 'minimal';
ALTER ROLE other_authenticator SET pgrst.db_aggregates_enabled = 'false'; ALTER ROLE other_authenticator SET pgrst.db_aggregates_enabled = 'false';
ALTER ROLE other_authenticator SET pgrst.db_extra_search_path = 'public, extensions, other'; ALTER ROLE other_authenticator SET pgrst.db_extra_search_path = 'public, extensions, other';
ALTER ROLE other_authenticator SET pgrst.db_max_rows = '100'; ALTER ROLE other_authenticator SET pgrst.db_max_rows = '100';
+11
View File
@@ -286,6 +286,17 @@ def test_jwt_secret_min_length(defaultenv):
assert "The JWT secret must be at least 32 characters long." in error assert "The JWT secret must be at least 32 characters long." in error
def test_invalid_client_error_verbosity(defaultenv):
"Given an invalid value for client-error-verbosity, Postgrest should exit with a non-zero exit code."
env = {
**defaultenv,
"PGRST_CLIENT_ERROR_VERBOSITY": "invalid",
}
error = cli(["--dump-config"], env=env, expect_error=True)
assert "Invalid client-error-verbosity. Check your configuration." in error
@pytest.mark.parametrize("restricted_schema", FIXTURES["restrictedschemas"]) @pytest.mark.parametrize("restricted_schema", FIXTURES["restrictedschemas"])
def test_restricted_db_schemas(restricted_schema, defaultenv): def test_restricted_db_schemas(restricted_schema, defaultenv):
"Should print error when db-schemas config contain pg_catalog or information_schema" "Should print error when db-schemas config contain pg_catalog or information_schema"
+32
View File
@@ -1757,3 +1757,35 @@ def test_server_timing_transaction_duration(defaultenv, metapostgrest):
] ]
assert 2000 <= response_dur < 3000 assert 2000 <= response_dur < 3000
def test_client_error_verbosity_config(defaultenv):
"Test PostgREST errors with different error verbosity settings"
env = {
**defaultenv,
"PGRST_CLIENT_ERROR_VERBOSITY": "minimal", # hide details and hint
}
with run(env=env) as postgrest:
response = postgrest.session.get("/itemsxx")
assert response.status_code == 404
assert response.json() == {
"code": "PGRST205",
"message": "Could not find the table 'public.itemsxx' in the schema cache",
}
env = {
**defaultenv,
"PGRST_CLIENT_ERROR_VERBOSITY": "verbose",
}
with run(env=env) as postgrest:
response = postgrest.session.get("/itemsxx")
assert response.status_code == 404
assert response.json() == {
"code": "PGRST205",
"message": "Could not find the table 'public.itemsxx' in the schema cache",
"details": None,
"hint": "Perhaps you meant the table 'public.items'",
}
+2 -1
View File
@@ -40,7 +40,7 @@ import PostgREST.Config (AppConfig (..),
JSPathExp (..), JSPathExp (..),
LogLevel (..), LogLevel (..),
OpenAPIMode (..), OpenAPIMode (..),
parseSecret) Verbosity (..), parseSecret)
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..)) import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
import Prometheus (Counter, getCounter) import Prometheus (Counter, getCounter)
import Protolude hiding (get, toS) import Protolude hiding (get, toS)
@@ -121,6 +121,7 @@ baseCfg :: AppConfig
baseCfg = let secret = encodeUtf8 "reallyreallyreallyreallyverysafe" in baseCfg = let secret = encodeUtf8 "reallyreallyreallyreallyverysafe" in
AppConfig { AppConfig {
configAppSettings = [ ("app.settings.app_host", "localhost") , ("app.settings.external_api_secret", "0123456789abcdef") ] configAppSettings = [ ("app.settings.app_host", "localhost") , ("app.settings.external_api_secret", "0123456789abcdef") ]
, configClientErrorVerbosity = Verbose
, configDbAggregates = False , configDbAggregates = False
, configDbAnonRole = Just "postgrest_test_anonymous" , configDbAnonRole = Just "postgrest_test_anonymous"
, configDbChannel = mempty , configDbChannel = mempty