From 83dc082acf6339f86476a5eef089b83d9ddc0480 Mon Sep 17 00:00:00 2001 From: Taimoor Zaeem Date: Tue, 24 Feb 2026 20:01:55 +0500 Subject: [PATCH] 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 --- CHANGELOG.md | 1 + docs/references/configuration.rst | 47 +++++++++++++++++++ src/PostgREST/App.hs | 45 +++++++++--------- src/PostgREST/Config.hs | 26 +++++++++- src/PostgREST/Config/Database.hs | 1 + src/PostgREST/Error.hs | 19 +++++--- src/PostgREST/Error/Types.hs | 1 - src/PostgREST/Observation.hs | 5 +- src/PostgREST/Response.hs | 8 ++-- test/io/configs/expected/aliases.config | 1 + .../configs/expected/boolean-numeric.config | 1 + .../io/configs/expected/boolean-string.config | 1 + test/io/configs/expected/defaults.config | 1 + .../expected/jwt-role-claim-key1.config | 1 + .../expected/jwt-role-claim-key2.config | 1 + .../expected/jwt-role-claim-key3.config | 1 + .../expected/jwt-role-claim-key4.config | 1 + .../expected/jwt-role-claim-key5.config | 1 + ...efaults-with-db-other-authenticator.config | 1 + .../expected/no-defaults-with-db.config | 1 + test/io/configs/expected/no-defaults.config | 1 + test/io/configs/expected/types.config | 1 + test/io/configs/expected/utf-8.config | 1 + test/io/configs/no-defaults-env.yaml | 1 + test/io/configs/no-defaults.config | 1 + test/io/fixtures/db_config.sql | 2 + test/io/test_cli.py | 11 +++++ test/io/test_io.py | 32 +++++++++++++ test/spec/SpecHelper.hs | 3 +- 29 files changed, 179 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a74472ac9..d1d9bf949 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 - 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`. +- Add config `client_error_verbosity` to customize error verbosity by @taimoorzaeem in #4088 ### Changed diff --git a/docs/references/configuration.rst b/docs/references/configuration.rst index ed51daaa6..1a6a1693f 100644 --- a/docs/references/configuration.rst +++ b/docs/references/configuration.rst @@ -195,6 +195,53 @@ app.settings.* The :code:`current_setting` function has `an optional boolean second `_ 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 ` 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 diff --git a/src/PostgREST/App.hs b/src/PostgREST/App.hs index 9f207de1f..44fdb2a9e 100644 --- a/src/PostgREST/App.hs +++ b/src/PostgREST/App.hs @@ -119,30 +119,31 @@ postgrest logLevel appState connWorker = Logger.middleware logLevel Auth.getRole $ -- fromJust can be used, because the auth middleware will **always** add -- some AuthResult to the vault. - \req respond -> case fromJust $ Auth.getResult req of - Left err -> respond $ Error.errorResponseFor err - Right authResult -> do - appConf <- AppState.getConfig appState -- the config must be read again because it can reload - maybeSchemaCache <- AppState.getSchemaCache appState + \req respond -> do + appConf@AppConfig{..} <- AppState.getConfig appState -- the config must be read again because it can reload + case fromJust $ Auth.getResult req of + Left err -> respond $ Error.errorResponseFor configClientErrorVerbosity err + Right authResult -> do + maybeSchemaCache <- AppState.getSchemaCache appState - let - eitherResponse :: IO (Either Error Wai.Response) - eitherResponse = - runExceptT $ postgrestResponse appState appConf maybeSchemaCache authResult req + let + eitherResponse :: IO (Either Error Wai.Response) + eitherResponse = + runExceptT $ postgrestResponse appState appConf maybeSchemaCache authResult req - response <- either Error.errorResponseFor identity <$> eitherResponse - -- Launch the connWorker when the connection is down. The postgrest - -- function can respond successfully (with a stale schema cache) before - -- the connWorker is done. However, when there's an empty schema cache - -- postgrest responds with the error `PGRST002`; this means that the schema - -- 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 - -- 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 - resp <- do - delay <- AppState.getNextDelay appState - return $ addRetryHint delay response - respond resp + response <- either (Error.errorResponseFor configClientErrorVerbosity) identity <$> eitherResponse + -- Launch the connWorker when the connection is down. The postgrest + -- function can respond successfully (with a stale schema cache) before + -- the connWorker is done. However, when there's an empty schema cache + -- postgrest responds with the error `PGRST002`; this means that the schema + -- 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 + -- 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 + resp <- do + delay <- AppState.getNextDelay appState + return $ addRetryHint delay response + respond resp postgrestResponse :: AppState.AppState diff --git a/src/PostgREST/Config.hs b/src/PostgREST/Config.hs index a5706a74d..fbc685309 100644 --- a/src/PostgREST/Config.hs +++ b/src/PostgREST/Config.hs @@ -29,6 +29,7 @@ module PostgREST.Config , addTargetSessionAttrs , exampleConfigFile , audMatchesCfg + , Verbosity (..) ) where import qualified Data.Aeson as JSON @@ -73,6 +74,7 @@ audMatchesCfg = maybe (const True) (==) . configJwtAudience data AppConfig = AppConfig { configAppSettings :: [(Text, Text)] + , configClientErrorVerbosity :: Verbosity , configDbAggregates :: Bool , configDbAnonRole :: Maybe BS.ByteString , configDbChannel :: Text @@ -134,6 +136,15 @@ dumpLogLevel = \case LogInfo -> "info" LogDebug -> "debug" +data Verbosity + = Minimal + | Verbose + +dumpClientErrorVerbosity :: Verbosity -> Text +dumpClientErrorVerbosity = \case + Minimal -> "minimal" + Verbose -> "verbose" + data OpenAPIMode = OAFollowPriv | OAIgnorePriv | OADisabled deriving Eq @@ -150,7 +161,8 @@ toText conf = where -- apply conf to all pgrst settings 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-channel", q . configDbChannel) ,("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 = AppConfig <$> parseAppSettings "app.settings" + <*> parseErrorVerbosity "client-error-verbosity" <*> (fromMaybe False <$> optBool "db-aggregates-enabled") <*> (fmap encodeUtf8 <$> optString "db-anon-role") <*> (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-relationship-load-sleep" 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 key = addFromEnv . fmap (fmap coerceText) <$> C.subassocs key C.value 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-port = 3001" , "" + , "# PostgREST error json verbosity config" + , "# client-error-verbosity = \"verbose\"" + , "" , "## The database role to use when no client authentication is provided" , "# db-anon-role = \"anon\"" , "" diff --git a/src/PostgREST/Config/Database.hs b/src/PostgREST/Config/Database.hs index aff4b5b8a..e25cb87f2 100644 --- a/src/PostgREST/Config/Database.hs +++ b/src/PostgREST/Config/Database.hs @@ -46,6 +46,7 @@ dbSettingsNames :: [Text] dbSettingsNames = (prefix <>) <$> ["db_aggregates_enabled" + ,"client_error_verbosity" ,"db_anon_role" ,"db_pre_config" ,"db_extra_search_path" diff --git a/src/PostgREST/Error.hs b/src/PostgREST/Error.hs index 0bfcb72c8..a35956814 100644 --- a/src/PostgREST/Error.hs +++ b/src/PostgREST/Error.hs @@ -42,6 +42,7 @@ import Network.HTTP.Types.Header (Header) import PostgREST.MediaType (MediaType (..)) import qualified PostgREST.MediaType as MediaType +import PostgREST.Config (Verbosity (..)) import PostgREST.SchemaCache (SchemaCache (SchemaCache, dbTablesFuzzyIndex)) import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..), Schema) @@ -57,26 +58,30 @@ import PostgREST.Error.Types import Protolude -- | Encode Error to ByteString -errorPayload :: (ErrorBody a, ErrorHeaders a) => a -> LByteString -errorPayload = JSON.encode . toJsonPgrstError +errorPayload :: (ErrorBody a, ErrorHeaders a) => Verbosity -> a -> LByteString +errorPayload verb = JSON.encode . toJsonPgrstError verb where - toJsonPgrstError :: (ErrorBody a, ErrorHeaders a) => a -> JSON.Value - toJsonPgrstError err = JSON.object [ + toJsonPgrstError :: (ErrorBody a, ErrorHeaders a) => Verbosity -> a -> JSON.Value + toJsonPgrstError Verbose err = JSON.object [ "code" .= code err , "message" .= message err , "details" .= details err , "hint" .= hint err ] + toJsonPgrstError Minimal err = JSON.object [ + "code" .= code err + , "message" .= message err + ] -- | Create HTTP response from Error -errorResponseFor :: (ErrorBody a, ErrorHeaders a) => a -> Response -errorResponseFor err = +errorResponseFor :: (ErrorBody a, ErrorHeaders a) => Verbosity -> a -> Response +errorResponseFor verb err = let baseHeader = MediaType.toContentType MTApplicationJSON cLHeader body = (,) "Content-Length" (show $ LBS.length body) :: Header pSHeader code' = ("Proxy-Status", "PostgREST; error=" <> T.encodeUtf8 code') 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 status :: a -> HTTP.Status diff --git a/src/PostgREST/Error/Types.hs b/src/PostgREST/Error/Types.hs index 778e558f2..eb2f13dbc 100644 --- a/src/PostgREST/Error/Types.hs +++ b/src/PostgREST/Error/Types.hs @@ -25,7 +25,6 @@ import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..)) import PostgREST.SchemaCache.Relationship (Relationship (..), RelationshipsMap) import PostgREST.SchemaCache.Routine (Routine (..)) - import Protolude data Error diff --git a/src/PostgREST/Observation.hs b/src/PostgREST/Observation.hs index 0878671c3..ad990308c 100644 --- a/src/PostgREST/Observation.hs +++ b/src/PostgREST/Observation.hs @@ -24,6 +24,7 @@ import qualified Hasql.Pool as SQL import qualified Hasql.Pool.Observation as SQL import Network.HTTP.Types.Status (Status) import Numeric (showFFloat) +import PostgREST.Config (Verbosity (..)) import PostgREST.Config.PgVersion import qualified PostgREST.Error as Error import PostgREST.Query (MainQuery) @@ -94,7 +95,7 @@ observationMessage = \case ExitDBFatalError ServerError08P01 usageErr -> "Connection poolers in statement mode are not supported." <> jsonMessage usageErr SchemaCacheEmptyObs -> - T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.NoSchemaCacheError + T.decodeUtf8 . LBS.toStrict . Error.errorPayload Verbose $ Error.NoSchemaCacheError SchemaCacheErrorObs dbSchemas extraPaths usageErr -> "Failed to load the schema cache using " <> "db-schemas=" <> T.intercalate "," (toList dbSchemas) @@ -167,7 +168,7 @@ observationMessage = \case showMillis :: Double -> Text 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 diff --git a/src/PostgREST/Response.hs b/src/PostgREST/Response.hs index 24ee29b4f..bae52d28d 100644 --- a/src/PostgREST/Response.hs +++ b/src/PostgREST/Response.hs @@ -62,7 +62,7 @@ data PgrstResponse = 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 (status, contentRange) = RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal cLHeader = if headersOnly then mempty else [ contentLengthHeader bod ] @@ -79,7 +79,7 @@ actionResponse (DbCrudResult plan@WrappedReadPlan{pMedia, wrHdrsOnly=headersOnly ++ cLHeader ++ contentTypeHeaders pMedia ctxApiRequest ++ 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) | headersOnly = mempty | otherwise = LBS.fromStrict rsBody @@ -178,12 +178,12 @@ actionResponse (DbCrudResult plan@MutateReadPlan{mrMutation=MutationDelete, pMed 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 (status, contentRange) = RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal 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) else LBS.fromStrict rsBody isHeadMethod = invMethod == InvRead True diff --git a/test/io/configs/expected/aliases.config b/test/io/configs/expected/aliases.config index 0655e5c4b..ab6a6f150 100644 --- a/test/io/configs/expected/aliases.config +++ b/test/io/configs/expected/aliases.config @@ -1,3 +1,4 @@ +client-error-verbosity = "verbose" db-aggregates-enabled = false db-anon-role = "" db-channel = "pgrst" diff --git a/test/io/configs/expected/boolean-numeric.config b/test/io/configs/expected/boolean-numeric.config index 53a13a7b8..22bc20491 100644 --- a/test/io/configs/expected/boolean-numeric.config +++ b/test/io/configs/expected/boolean-numeric.config @@ -1,3 +1,4 @@ +client-error-verbosity = "verbose" db-aggregates-enabled = false db-anon-role = "" db-channel = "pgrst" diff --git a/test/io/configs/expected/boolean-string.config b/test/io/configs/expected/boolean-string.config index 53a13a7b8..22bc20491 100644 --- a/test/io/configs/expected/boolean-string.config +++ b/test/io/configs/expected/boolean-string.config @@ -1,3 +1,4 @@ +client-error-verbosity = "verbose" db-aggregates-enabled = false db-anon-role = "" db-channel = "pgrst" diff --git a/test/io/configs/expected/defaults.config b/test/io/configs/expected/defaults.config index 87909425e..acbd17508 100644 --- a/test/io/configs/expected/defaults.config +++ b/test/io/configs/expected/defaults.config @@ -1,3 +1,4 @@ +client-error-verbosity = "verbose" db-aggregates-enabled = false db-anon-role = "" db-channel = "pgrst" diff --git a/test/io/configs/expected/jwt-role-claim-key1.config b/test/io/configs/expected/jwt-role-claim-key1.config index 319a4932c..6e9bc26f4 100644 --- a/test/io/configs/expected/jwt-role-claim-key1.config +++ b/test/io/configs/expected/jwt-role-claim-key1.config @@ -1,3 +1,4 @@ +client-error-verbosity = "verbose" db-aggregates-enabled = false db-anon-role = "" db-channel = "pgrst" diff --git a/test/io/configs/expected/jwt-role-claim-key2.config b/test/io/configs/expected/jwt-role-claim-key2.config index 535dc9e24..938ab2f1d 100644 --- a/test/io/configs/expected/jwt-role-claim-key2.config +++ b/test/io/configs/expected/jwt-role-claim-key2.config @@ -1,3 +1,4 @@ +client-error-verbosity = "verbose" db-aggregates-enabled = false db-anon-role = "" db-channel = "pgrst" diff --git a/test/io/configs/expected/jwt-role-claim-key3.config b/test/io/configs/expected/jwt-role-claim-key3.config index c052f2dfc..c0fcd2c4b 100644 --- a/test/io/configs/expected/jwt-role-claim-key3.config +++ b/test/io/configs/expected/jwt-role-claim-key3.config @@ -1,3 +1,4 @@ +client-error-verbosity = "verbose" db-aggregates-enabled = false db-anon-role = "" db-channel = "pgrst" diff --git a/test/io/configs/expected/jwt-role-claim-key4.config b/test/io/configs/expected/jwt-role-claim-key4.config index a3f8e8df5..3168929b6 100644 --- a/test/io/configs/expected/jwt-role-claim-key4.config +++ b/test/io/configs/expected/jwt-role-claim-key4.config @@ -1,3 +1,4 @@ +client-error-verbosity = "verbose" db-aggregates-enabled = false db-anon-role = "" db-channel = "pgrst" diff --git a/test/io/configs/expected/jwt-role-claim-key5.config b/test/io/configs/expected/jwt-role-claim-key5.config index 0cfcd55da..b3460f6f6 100644 --- a/test/io/configs/expected/jwt-role-claim-key5.config +++ b/test/io/configs/expected/jwt-role-claim-key5.config @@ -1,3 +1,4 @@ +client-error-verbosity = "verbose" db-aggregates-enabled = false db-anon-role = "" db-channel = "pgrst" diff --git a/test/io/configs/expected/no-defaults-with-db-other-authenticator.config b/test/io/configs/expected/no-defaults-with-db-other-authenticator.config index ebb54e0ea..6896b78e5 100644 --- a/test/io/configs/expected/no-defaults-with-db-other-authenticator.config +++ b/test/io/configs/expected/no-defaults-with-db-other-authenticator.config @@ -1,3 +1,4 @@ +client-error-verbosity = "minimal" db-aggregates-enabled = false db-anon-role = "pre_config_role" db-channel = "postgrest" diff --git a/test/io/configs/expected/no-defaults-with-db.config b/test/io/configs/expected/no-defaults-with-db.config index 9077fdbde..86aede755 100644 --- a/test/io/configs/expected/no-defaults-with-db.config +++ b/test/io/configs/expected/no-defaults-with-db.config @@ -1,3 +1,4 @@ +client-error-verbosity = "minimal" db-aggregates-enabled = false db-anon-role = "anonymous" db-channel = "postgrest" diff --git a/test/io/configs/expected/no-defaults.config b/test/io/configs/expected/no-defaults.config index 2b0ab43a9..a131be048 100644 --- a/test/io/configs/expected/no-defaults.config +++ b/test/io/configs/expected/no-defaults.config @@ -1,3 +1,4 @@ +client-error-verbosity = "minimal" db-aggregates-enabled = true db-anon-role = "root" db-channel = "postgrest" diff --git a/test/io/configs/expected/types.config b/test/io/configs/expected/types.config index cb474bcdd..2d00d680d 100644 --- a/test/io/configs/expected/types.config +++ b/test/io/configs/expected/types.config @@ -1,3 +1,4 @@ +client-error-verbosity = "verbose" db-aggregates-enabled = false db-anon-role = "" db-channel = "pgrst" diff --git a/test/io/configs/expected/utf-8.config b/test/io/configs/expected/utf-8.config index dc8226011..c06c50f3b 100644 --- a/test/io/configs/expected/utf-8.config +++ b/test/io/configs/expected/utf-8.config @@ -1,3 +1,4 @@ +client-error-verbosity = "verbose" db-aggregates-enabled = false db-anon-role = "" db-channel = "pgrst" diff --git a/test/io/configs/no-defaults-env.yaml b/test/io/configs/no-defaults-env.yaml index bda8e9b28..d9fea4c43 100644 --- a/test/io/configs/no-defaults-env.yaml +++ b/test/io/configs/no-defaults-env.yaml @@ -1,5 +1,6 @@ PGRST_APP_SETTINGS_test2: test PGRST_APP_SETTINGS_test: test +PGRST_CLIENT_ERROR_VERBOSITY: minimal PGRST_DB_AGGREGATES_ENABLED: true PGRST_DB_ANON_ROLE: root PGRST_DB_CHANNEL: postgrest diff --git a/test/io/configs/no-defaults.config b/test/io/configs/no-defaults.config index ceeb5dbdb..f0e03e42e 100644 --- a/test/io/configs/no-defaults.config +++ b/test/io/configs/no-defaults.config @@ -1,3 +1,4 @@ +client-error-verbosity = "minimal" db-aggregates-enabled = true db-anon-role = "root" db-channel = "postgrest" diff --git a/test/io/fixtures/db_config.sql b/test/io/fixtures/db_config.sql index 1c234ba72..bf7880f43 100644 --- a/test/io/fixtures/db_config.sql +++ b/test/io/fixtures/db_config.sql @@ -2,6 +2,7 @@ CREATE ROLE db_config_authenticator LOGIN NOINHERIT; -- reloadable config options -- 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_anon_role = 'anonymous'; 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 -- these settings will override the values in configs/no-defaults.config, so they must be different 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_extra_search_path = 'public, extensions, other'; ALTER ROLE other_authenticator SET pgrst.db_max_rows = '100'; diff --git a/test/io/test_cli.py b/test/io/test_cli.py index 8135fcce2..72351fc35 100644 --- a/test/io/test_cli.py +++ b/test/io/test_cli.py @@ -286,6 +286,17 @@ def test_jwt_secret_min_length(defaultenv): 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"]) def test_restricted_db_schemas(restricted_schema, defaultenv): "Should print error when db-schemas config contain pg_catalog or information_schema" diff --git a/test/io/test_io.py b/test/io/test_io.py index ba55727b9..79b386062 100644 --- a/test/io/test_io.py +++ b/test/io/test_io.py @@ -1757,3 +1757,35 @@ def test_server_timing_transaction_duration(defaultenv, metapostgrest): ] 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'", + } diff --git a/test/spec/SpecHelper.hs b/test/spec/SpecHelper.hs index a35d7db92..005016495 100644 --- a/test/spec/SpecHelper.hs +++ b/test/spec/SpecHelper.hs @@ -40,7 +40,7 @@ import PostgREST.Config (AppConfig (..), JSPathExp (..), LogLevel (..), OpenAPIMode (..), - parseSecret) + Verbosity (..), parseSecret) import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..)) import Prometheus (Counter, getCounter) import Protolude hiding (get, toS) @@ -121,6 +121,7 @@ baseCfg :: AppConfig baseCfg = let secret = encodeUtf8 "reallyreallyreallyreallyverysafe" in AppConfig { configAppSettings = [ ("app.settings.app_host", "localhost") , ("app.settings.external_api_secret", "0123456789abcdef") ] + , configClientErrorVerbosity = Verbose , configDbAggregates = False , configDbAnonRole = Just "postgrest_test_anonymous" , configDbChannel = mempty