fix: empty error messages for disabled openapi and /invalid/nested/paths
This commit is contained in:
@@ -49,6 +49,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
+ Return `PGRST301` error when `Bearer` in auth header is sent empty
|
+ Return `PGRST301` error when `Bearer` in auth header is sent empty
|
||||||
+ Diagnostic error messages instead of exposed internals
|
+ Diagnostic error messages instead of exposed internals
|
||||||
+ Return new `PGRST303` error when jwt claims decoding fails
|
+ Return new `PGRST303` error when jwt claims decoding fails
|
||||||
|
- #3906, Return `PGRST125` and `PGRST126` errors instead of empty json - @taimoorzaeem
|
||||||
|
|
||||||
## [12.2.8] - 2025-02-10
|
## [12.2.8] - 2025-02-10
|
||||||
|
|
||||||
|
|||||||
@@ -257,6 +257,15 @@ Related to the HTTP request elements.
|
|||||||
| | | See :ref:`prefer_max_affected`. |
|
| | | See :ref:`prefer_max_affected`. |
|
||||||
| PGRST124 | | |
|
| PGRST124 | | |
|
||||||
+---------------+-------------+-------------------------------------------------------------+
|
+---------------+-------------+-------------------------------------------------------------+
|
||||||
|
| .. _pgrst125: | 404 | Invalid path is specified in request URL. |
|
||||||
|
| | | |
|
||||||
|
| PGRST125 | | |
|
||||||
|
+---------------+-------------+-------------------------------------------------------------+
|
||||||
|
| .. _pgrst126: | 404 | Open API config is disabled but API root path is |
|
||||||
|
| | | accessed. See :ref:`openapi-mode`. |
|
||||||
|
| PGRST126 | | |
|
||||||
|
+---------------+-------------+-------------------------------------------------------------+
|
||||||
|
|
||||||
|
|
||||||
.. _pgrst2**:
|
.. _pgrst2**:
|
||||||
|
|
||||||
|
|||||||
@@ -165,13 +165,15 @@ userApiRequest conf req reqBody timezones = do
|
|||||||
|
|
||||||
getResource :: AppConfig -> [Text] -> Either ApiRequestError Resource
|
getResource :: AppConfig -> [Text] -> Either ApiRequestError Resource
|
||||||
getResource AppConfig{configOpenApiMode, configDbRootSpec} = \case
|
getResource AppConfig{configOpenApiMode, configDbRootSpec} = \case
|
||||||
[] -> case configDbRootSpec of
|
[] ->
|
||||||
Just (QualifiedIdentifier _ pathName) -> Right $ ResourceRoutine pathName
|
case (configOpenApiMode,configDbRootSpec) of
|
||||||
Nothing | configOpenApiMode == OADisabled -> Left NotFound
|
(OADisabled,_) -> Left OpenAPIDisabled
|
||||||
| otherwise -> Right ResourceSchema
|
(_, Just qi) -> Right $ ResourceRoutine (qiName qi)
|
||||||
|
(_, Nothing) -> Right ResourceSchema
|
||||||
|
|
||||||
[table] -> Right $ ResourceRelation table
|
[table] -> Right $ ResourceRelation table
|
||||||
["rpc", pName] -> Right $ ResourceRoutine pName
|
["rpc", pName] -> Right $ ResourceRoutine pName
|
||||||
_ -> Left NotFound
|
_ -> Left InvalidResourcePath
|
||||||
|
|
||||||
getAction :: Resource -> Schema -> ByteString -> Either ApiRequestError Action
|
getAction :: Resource -> Schema -> ByteString -> Either ApiRequestError Action
|
||||||
getAction resource schema method =
|
getAction resource schema method =
|
||||||
|
|||||||
+20
-4
@@ -72,7 +72,6 @@ data ApiRequestError
|
|||||||
| InvalidPreferences [ByteString]
|
| InvalidPreferences [ByteString]
|
||||||
| InvalidRange RangeError
|
| InvalidRange RangeError
|
||||||
| InvalidRpcMethod ByteString
|
| InvalidRpcMethod ByteString
|
||||||
| NotFound
|
|
||||||
| NoRelBetween Text Text (Maybe Text) Text RelationshipsMap
|
| NoRelBetween Text Text (Maybe Text) Text RelationshipsMap
|
||||||
| NoRpc Text Text [Text] MediaType Bool [QualifiedIdentifier] [Routine]
|
| NoRpc Text Text [Text] MediaType Bool [QualifiedIdentifier] [Routine]
|
||||||
| NotEmbedded Text
|
| NotEmbedded Text
|
||||||
@@ -91,6 +90,8 @@ data ApiRequestError
|
|||||||
| SingularityError Integer
|
| SingularityError Integer
|
||||||
| PGRSTParseError RaiseError
|
| PGRSTParseError RaiseError
|
||||||
| MaxAffectedViolationError Integer
|
| MaxAffectedViolationError Integer
|
||||||
|
| InvalidResourcePath
|
||||||
|
| OpenAPIDisabled
|
||||||
deriving Show
|
deriving Show
|
||||||
|
|
||||||
data QPError = QPError Text Text
|
data QPError = QPError Text Text
|
||||||
@@ -118,7 +119,6 @@ instance PgrstError ApiRequestError where
|
|||||||
status InvalidPreferences{} = HTTP.status400
|
status InvalidPreferences{} = HTTP.status400
|
||||||
status InvalidRpcMethod{} = HTTP.status405
|
status InvalidRpcMethod{} = HTTP.status405
|
||||||
status InvalidRange{} = HTTP.status416
|
status InvalidRange{} = HTTP.status416
|
||||||
status NotFound = HTTP.status404
|
|
||||||
|
|
||||||
status NoRelBetween{} = HTTP.status400
|
status NoRelBetween{} = HTTP.status400
|
||||||
status NoRpc{} = HTTP.status404
|
status NoRpc{} = HTTP.status404
|
||||||
@@ -138,6 +138,8 @@ instance PgrstError ApiRequestError where
|
|||||||
status SingularityError{} = HTTP.status406
|
status SingularityError{} = HTTP.status406
|
||||||
status PGRSTParseError{} = HTTP.status500
|
status PGRSTParseError{} = HTTP.status500
|
||||||
status MaxAffectedViolationError{} = HTTP.status400
|
status MaxAffectedViolationError{} = HTTP.status400
|
||||||
|
status InvalidResourcePath = HTTP.status404
|
||||||
|
status OpenAPIDisabled = HTTP.status404
|
||||||
|
|
||||||
headers _ = mempty
|
headers _ = mempty
|
||||||
|
|
||||||
@@ -177,8 +179,6 @@ instance JSON.ToJSON ApiRequestError where
|
|||||||
toJSON (MediaTypeError cts) = toJsonPgrstError
|
toJSON (MediaTypeError cts) = toJsonPgrstError
|
||||||
ApiRequestErrorCode07 ("None of these media types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts)) Nothing Nothing
|
ApiRequestErrorCode07 ("None of these media types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts)) Nothing Nothing
|
||||||
|
|
||||||
toJSON NotFound = JSON.object []
|
|
||||||
|
|
||||||
toJSON (NotEmbedded resource) = toJsonPgrstError
|
toJSON (NotEmbedded resource) = toJsonPgrstError
|
||||||
ApiRequestErrorCode08
|
ApiRequestErrorCode08
|
||||||
("'" <> resource <> "' is not an embedded resource in this request")
|
("'" <> resource <> "' is not an embedded resource in this request")
|
||||||
@@ -245,6 +245,18 @@ instance JSON.ToJSON ApiRequestError where
|
|||||||
(Just $ JSON.String $ T.unwords ["The query affects", show n, "rows"])
|
(Just $ JSON.String $ T.unwords ["The query affects", show n, "rows"])
|
||||||
Nothing
|
Nothing
|
||||||
|
|
||||||
|
toJSON InvalidResourcePath = toJsonPgrstError
|
||||||
|
ApiRequestErrorCode25
|
||||||
|
"Invalid path specified in request URL"
|
||||||
|
Nothing
|
||||||
|
Nothing
|
||||||
|
|
||||||
|
toJSON OpenAPIDisabled = toJsonPgrstError
|
||||||
|
ApiRequestErrorCode26
|
||||||
|
"Root endpoint metadata is disabled"
|
||||||
|
Nothing
|
||||||
|
Nothing
|
||||||
|
|
||||||
toJSON (NoRelBetween parent child embedHint schema allRels) = toJsonPgrstError
|
toJSON (NoRelBetween parent child embedHint schema allRels) = toJsonPgrstError
|
||||||
SchemaCacheErrorCode00
|
SchemaCacheErrorCode00
|
||||||
("Could not find a relationship between '" <> parent <> "' and '" <> child <> "' in the schema cache")
|
("Could not find a relationship between '" <> parent <> "' and '" <> child <> "' in the schema cache")
|
||||||
@@ -706,6 +718,8 @@ data ErrorCode
|
|||||||
| ApiRequestErrorCode22
|
| ApiRequestErrorCode22
|
||||||
| ApiRequestErrorCode23
|
| ApiRequestErrorCode23
|
||||||
| ApiRequestErrorCode24
|
| ApiRequestErrorCode24
|
||||||
|
| ApiRequestErrorCode25
|
||||||
|
| ApiRequestErrorCode26
|
||||||
-- Schema Cache errors
|
-- Schema Cache errors
|
||||||
| SchemaCacheErrorCode00
|
| SchemaCacheErrorCode00
|
||||||
| SchemaCacheErrorCode01
|
| SchemaCacheErrorCode01
|
||||||
@@ -755,6 +769,8 @@ buildErrorCode code = case code of
|
|||||||
ApiRequestErrorCode22 -> "PGRST122"
|
ApiRequestErrorCode22 -> "PGRST122"
|
||||||
ApiRequestErrorCode23 -> "PGRST123"
|
ApiRequestErrorCode23 -> "PGRST123"
|
||||||
ApiRequestErrorCode24 -> "PGRST124"
|
ApiRequestErrorCode24 -> "PGRST124"
|
||||||
|
ApiRequestErrorCode25 -> "PGRST125"
|
||||||
|
ApiRequestErrorCode26 -> "PGRST126"
|
||||||
|
|
||||||
SchemaCacheErrorCode00 -> "PGRST200"
|
SchemaCacheErrorCode00 -> "PGRST200"
|
||||||
SchemaCacheErrorCode01 -> "PGRST201"
|
SchemaCacheErrorCode01 -> "PGRST201"
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import qualified PostgREST.SchemaCache.Routine as Routine
|
|||||||
|
|
||||||
import Data.Either.Combinators (mapLeft, mapRight)
|
import Data.Either.Combinators (mapLeft, mapRight)
|
||||||
import Data.List (delete, lookup)
|
import Data.List (delete, lookup)
|
||||||
|
import Data.Maybe (fromJust)
|
||||||
import Data.Tree (Tree (..))
|
import Data.Tree (Tree (..))
|
||||||
|
|
||||||
import PostgREST.ApiRequest (Action (..),
|
import PostgREST.ApiRequest (Action (..),
|
||||||
@@ -964,18 +965,18 @@ mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{
|
|||||||
if preferRepresentation == Just None || isNothing preferRepresentation
|
if preferRepresentation == Just None || isNothing preferRepresentation
|
||||||
then []
|
then []
|
||||||
else inferColsEmbedNeeds readReq pkCols
|
else inferColsEmbedNeeds readReq pkCols
|
||||||
tbl = HM.lookup qi dbTables
|
-- TODO: remove fromJust by refactoring later
|
||||||
pkCols = maybe mempty tablePKCols tbl
|
-- we can use fromJust, we have already looked up the table before building mutatePlan
|
||||||
|
tbl = fromJust $ HM.lookup qi dbTables
|
||||||
|
pkCols = maybe mempty tablePKCols (Just tbl)
|
||||||
logic = map (resolveLogicTree ctx . snd) qsLogic
|
logic = map (resolveLogicTree ctx . snd) qsLogic
|
||||||
combinedLogic = foldr (addFilterToLogicForest . resolveFilter ctx) logic qsFiltersRoot
|
combinedLogic = foldr (addFilterToLogicForest . resolveFilter ctx) logic qsFiltersRoot
|
||||||
body = payRaw <$> iPayload -- the body is assumed to be json at this stage(ApiRequest validates)
|
body = payRaw <$> iPayload -- the body is assumed to be json at this stage(ApiRequest validates)
|
||||||
applyDefaults = preferMissing == Just ApplyDefaults
|
applyDefaults = preferMissing == Just ApplyDefaults
|
||||||
typedColumnsOrError = resolveOrError ctx tbl `traverse` S.toList iColumns
|
typedColumnsOrError = resolveOrError ctx tbl `traverse` S.toList iColumns
|
||||||
|
|
||||||
resolveOrError :: ResolverContext -> Maybe Table -> FieldName -> Either ApiRequestError CoercibleField
|
resolveOrError :: ResolverContext -> Table -> FieldName -> Either ApiRequestError CoercibleField
|
||||||
resolveOrError _ Nothing _ = Left NotFound -- TODO: control never reaches here since #3869, should be fixed when fixing #3906
|
resolveOrError ctx table field = case resolveTableFieldName table field Nothing of
|
||||||
resolveOrError ctx (Just table) field =
|
|
||||||
case resolveTableFieldName table field Nothing of
|
|
||||||
CoercibleField{cfIRType=""} -> Left $ ColumnNotFound (tableName table) field
|
CoercibleField{cfIRType=""} -> Left $ ColumnNotFound (tableName table) field
|
||||||
cf -> Right $ withJsonParse ctx cf
|
cf -> Right $ withJsonParse ctx cf
|
||||||
|
|
||||||
|
|||||||
@@ -228,10 +228,10 @@ actionResponse (MaybeDbResult InspectPlan{ipHdrsOnly=headersOnly} body) _ versio
|
|||||||
(MediaType.toContentType MTOpenAPI : maybeToList (profileHeader schema negotiatedByProfile))
|
(MediaType.toContentType MTOpenAPI : maybeToList (profileHeader schema negotiatedByProfile))
|
||||||
(maybe mempty (\(x, y, z) -> if headersOnly then mempty else OpenAPI.encode versions conf sCache x y z) body)
|
(maybe mempty (\(x, y, z) -> if headersOnly then mempty else OpenAPI.encode versions conf sCache x y z) body)
|
||||||
|
|
||||||
actionResponse (NoDbResult (RelInfoPlan identifier)) _ _ _ sCache _ _ =
|
actionResponse (NoDbResult (RelInfoPlan qi@QualifiedIdentifier{..})) _ _ _ SchemaCache{dbTables} _ _ =
|
||||||
case HM.lookup identifier (dbTables sCache) of
|
case HM.lookup qi dbTables of
|
||||||
Just tbl -> respondInfo $ allowH tbl
|
Just tbl -> respondInfo $ allowH tbl
|
||||||
Nothing -> Left $ Error.ApiRequestError Error.NotFound
|
Nothing -> Left $ Error.ApiRequestError $ Error.TableNotFound qiSchema qiName (HM.elems dbTables)
|
||||||
where
|
where
|
||||||
allowH table =
|
allowH table =
|
||||||
let hasPK = not . null $ tablePKCols table in
|
let hasPK = not . null $ tablePKCols table in
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ module Feature.OpenApi.DisabledOpenApiSpec where
|
|||||||
import Network.HTTP.Types
|
import Network.HTTP.Types
|
||||||
import Network.Wai (Application)
|
import Network.Wai (Application)
|
||||||
|
|
||||||
import Test.Hspec hiding (pendingWith)
|
import Test.Hspec
|
||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
|
import Test.Hspec.Wai.JSON
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
@@ -13,4 +14,7 @@ spec =
|
|||||||
describe "Disabled OpenApi" $ do
|
describe "Disabled OpenApi" $ do
|
||||||
it "responds with 404" $
|
it "responds with 404" $
|
||||||
request methodGet "/"
|
request methodGet "/"
|
||||||
[("Accept","application/openapi+json")] "" `shouldRespondWith` 404
|
[("Accept","application/openapi+json")] ""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json| {"code":"PGRST126","details":null,"hint":null,"message":"Root endpoint metadata is disabled"} |]
|
||||||
|
{ matchStatus = 404 }
|
||||||
|
|||||||
@@ -1625,3 +1625,10 @@ spec = do
|
|||||||
get "/infinite_recursion?select=*" `shouldRespondWith`
|
get "/infinite_recursion?select=*" `shouldRespondWith`
|
||||||
[json|{"code":"42P17","message":"infinite recursion detected in rules for relation \"infinite_recursion\"","details":null,"hint":null}|]
|
[json|{"code":"42P17","message":"infinite recursion detected in rules for relation \"infinite_recursion\"","details":null,"hint":null}|]
|
||||||
{ matchStatus = 500 }
|
{ matchStatus = 500 }
|
||||||
|
|
||||||
|
context "invalid resource path" $ do
|
||||||
|
it "return http status 404" $
|
||||||
|
get "/first/second/third?select=*"
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json| {"code":"PGRST125","details":null,"hint":null,"message":"Invalid path specified in request URL"} |]
|
||||||
|
{ matchStatus = 404 }
|
||||||
|
|||||||
Reference in New Issue
Block a user