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
+23
-22
@@ -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
|
||||
|
||||
+25
-1
@@ -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\""
|
||||
, ""
|
||||
|
||||
@@ -46,6 +46,7 @@ dbSettingsNames :: [Text]
|
||||
dbSettingsNames =
|
||||
(prefix <>) <$>
|
||||
["db_aggregates_enabled"
|
||||
,"client_error_verbosity"
|
||||
,"db_anon_role"
|
||||
,"db_pre_config"
|
||||
,"db_extra_search_path"
|
||||
|
||||
+12
-7
@@ -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
|
||||
|
||||
@@ -25,7 +25,6 @@ import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
|
||||
import PostgREST.SchemaCache.Relationship (Relationship (..),
|
||||
RelationshipsMap)
|
||||
import PostgREST.SchemaCache.Routine (Routine (..))
|
||||
|
||||
import Protolude
|
||||
|
||||
data Error
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user