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:
committed by
Steve Chavez
parent
2edc44c352
commit
83dc082acf
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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\""
|
||||||
, ""
|
, ""
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,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 = "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,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,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,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,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';
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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'",
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user