feat: improve PGRST121 error message
* Clarify the message field * Show failed MESSAGE or DETAIL in the the PGRST121 error's details field * Show the correct JSON format in the hint field
This commit is contained in:
+6
-1
@@ -17,6 +17,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- #3435, Add log-level=debug, for development purposes - @steve-chavez
|
- #3435, Add log-level=debug, for development purposes - @steve-chavez
|
||||||
- #1526, Add `/metrics` endpoint on admin server - @steve-chavez
|
- #1526, Add `/metrics` endpoint on admin server - @steve-chavez
|
||||||
- Exposes connection pool metrics, schema cache metrics
|
- Exposes connection pool metrics, schema cache metrics
|
||||||
|
- #3404, Show the failed MESSAGE or DETAIL in the `details` field of the `PGRST121` (could not parse RAISE 'PGRST') error - @laurenceisla
|
||||||
|
- #3404, Show extra information in the `PGRST121` (could not parse RAISE 'PGRST') error - @laurenceisla
|
||||||
|
+ Shows the failed MESSAGE or DETAIL in the `details` field
|
||||||
|
+ Shows the correct JSON format in the `hints` field
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
@@ -30,10 +34,11 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- #3330, Incorrect admin server `/ready` response on slow schema cache loads - @steve-chavez
|
- #3330, Incorrect admin server `/ready` response on slow schema cache loads - @steve-chavez
|
||||||
- #3340, Log when the LISTEN channel gets a notification - @steve-chavez
|
- #3340, Log when the LISTEN channel gets a notification - @steve-chavez
|
||||||
- #3345, Fix in-database configuration values not loading for `pgrst.server_trace_header` and `pgrst.server_cors_allowed_origins` - @laurenceisla
|
- #3345, Fix in-database configuration values not loading for `pgrst.server_trace_header` and `pgrst.server_cors_allowed_origins` - @laurenceisla
|
||||||
- #3361, Clarify PGRST204(column not found) error message - @steve-chavez
|
- #3361, Clarify the `PGRST204` (column not found) error message - @steve-chavez
|
||||||
- #3373, Remove rejected mediatype `application/vnd.pgrst.object+json` from response - @taimoorzaeem
|
- #3373, Remove rejected mediatype `application/vnd.pgrst.object+json` from response - @taimoorzaeem
|
||||||
- #3418, Fix OpenAPI not tagging a FK column correctly on O2O relationships - @laurenceisla
|
- #3418, Fix OpenAPI not tagging a FK column correctly on O2O relationships - @laurenceisla
|
||||||
- #3256, Fix wrong http status for pg error `42P17 infinite recursion` - @taimoorzaeem
|
- #3256, Fix wrong http status for pg error `42P17 infinite recursion` - @taimoorzaeem
|
||||||
|
- #3404, Clarify the `PGRST121` (could not parse RAISE 'PGRST') error message - @laurenceisla
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ module PostgREST.ApiRequest.Types
|
|||||||
, OrderNulls(..)
|
, OrderNulls(..)
|
||||||
, OrderTerm(..)
|
, OrderTerm(..)
|
||||||
, QPError(..)
|
, QPError(..)
|
||||||
|
, RaiseError(..)
|
||||||
, RangeError(..)
|
, RangeError(..)
|
||||||
, SingleVal
|
, SingleVal
|
||||||
, TrileanVal(..)
|
, TrileanVal(..)
|
||||||
@@ -95,12 +96,17 @@ data ApiRequestError
|
|||||||
| OffLimitsChangesError Int64 Integer
|
| OffLimitsChangesError Int64 Integer
|
||||||
| PutMatchingPkError
|
| PutMatchingPkError
|
||||||
| SingularityError Integer
|
| SingularityError Integer
|
||||||
| PGRSTParseError
|
| PGRSTParseError RaiseError
|
||||||
| MaxAffectedViolationError Integer
|
| MaxAffectedViolationError Integer
|
||||||
deriving Show
|
deriving Show
|
||||||
|
|
||||||
data QPError = QPError Text Text
|
data QPError = QPError Text Text
|
||||||
deriving Show
|
deriving Show
|
||||||
|
data RaiseError
|
||||||
|
= MsgParseError ByteString
|
||||||
|
| DetParseError ByteString
|
||||||
|
| NoDetail
|
||||||
|
deriving Show
|
||||||
data RangeError
|
data RangeError
|
||||||
= NegativeLimit
|
= NegativeLimit
|
||||||
| LowerGTUpper
|
| LowerGTUpper
|
||||||
|
|||||||
+33
-17
@@ -34,6 +34,7 @@ import Network.HTTP.Types.Header (Header)
|
|||||||
|
|
||||||
import PostgREST.ApiRequest.Types (ApiRequestError (..),
|
import PostgREST.ApiRequest.Types (ApiRequestError (..),
|
||||||
QPError (..),
|
QPError (..),
|
||||||
|
RaiseError (..),
|
||||||
RangeError (..))
|
RangeError (..))
|
||||||
import PostgREST.MediaType (MediaType (..))
|
import PostgREST.MediaType (MediaType (..))
|
||||||
import qualified PostgREST.MediaType as MediaType
|
import qualified PostgREST.MediaType as MediaType
|
||||||
@@ -90,7 +91,7 @@ instance PgrstError ApiRequestError where
|
|||||||
status OffLimitsChangesError{} = HTTP.status400
|
status OffLimitsChangesError{} = HTTP.status400
|
||||||
status PutMatchingPkError = HTTP.status400
|
status PutMatchingPkError = HTTP.status400
|
||||||
status SingularityError{} = HTTP.status406
|
status SingularityError{} = HTTP.status406
|
||||||
status PGRSTParseError = HTTP.status500
|
status PGRSTParseError{} = HTTP.status500
|
||||||
status MaxAffectedViolationError{} = HTTP.status400
|
status MaxAffectedViolationError{} = HTTP.status400
|
||||||
|
|
||||||
headers _ = mempty
|
headers _ = mempty
|
||||||
@@ -187,8 +188,11 @@ instance JSON.ToJSON ApiRequestError where
|
|||||||
(Just "Only is null or not is null filters are allowed on embedded resources")
|
(Just "Only is null or not is null filters are allowed on embedded resources")
|
||||||
Nothing
|
Nothing
|
||||||
|
|
||||||
toJSON PGRSTParseError = toJsonPgrstError
|
toJSON (PGRSTParseError raiseErr) = toJsonPgrstError
|
||||||
ApiRequestErrorCode21 "The message and detail field of RAISE 'PGRST' error expects JSON" Nothing Nothing
|
ApiRequestErrorCode21
|
||||||
|
"Could not parse JSON in the \"RAISE SQLSTATE 'PGRST'\" error"
|
||||||
|
(Just $ JSON.String $ pgrstParseErrorDetails raiseErr)
|
||||||
|
(Just $ JSON.String $ pgrstParseErrorHint raiseErr)
|
||||||
|
|
||||||
toJSON (InvalidPreferences prefs) = toJsonPgrstError
|
toJSON (InvalidPreferences prefs) = toJsonPgrstError
|
||||||
ApiRequestErrorCode22
|
ApiRequestErrorCode22
|
||||||
@@ -387,6 +391,17 @@ relHint rels = T.intercalate ", " (hintList <$> rels)
|
|||||||
-- An ambiguousness error cannot happen for computed relationships TODO refactor so this mempty is not needed
|
-- An ambiguousness error cannot happen for computed relationships TODO refactor so this mempty is not needed
|
||||||
hintList ComputedRelationship{} = mempty
|
hintList ComputedRelationship{} = mempty
|
||||||
|
|
||||||
|
pgrstParseErrorDetails :: RaiseError -> Text
|
||||||
|
pgrstParseErrorDetails err = case err of
|
||||||
|
MsgParseError m -> "Invalid JSON value for MESSAGE: '" <> T.decodeUtf8 m <> "'"
|
||||||
|
DetParseError d -> "Invalid JSON value for DETAIL: '" <> T.decodeUtf8 d <> "'"
|
||||||
|
NoDetail -> "DETAIL is missing in the RAISE statement"
|
||||||
|
|
||||||
|
pgrstParseErrorHint :: RaiseError -> Text
|
||||||
|
pgrstParseErrorHint err = case err of
|
||||||
|
MsgParseError _ -> "MESSAGE must be a JSON object with obligatory keys: 'code', 'message' and optional keys: 'details', 'hint'."
|
||||||
|
_ -> "DETAIL must be a JSON object with obligatory keys: 'status', 'headers' and optional key: 'status_text'."
|
||||||
|
|
||||||
data PgError = PgError Authenticated SQL.UsageError
|
data PgError = PgError Authenticated SQL.UsageError
|
||||||
type Authenticated = Bool
|
type Authenticated = Bool
|
||||||
|
|
||||||
@@ -394,9 +409,9 @@ instance PgrstError PgError where
|
|||||||
status (PgError authed usageError) = pgErrorStatus authed usageError
|
status (PgError authed usageError) = pgErrorStatus authed usageError
|
||||||
|
|
||||||
headers (PgError _ (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError (SQL.ServerError "PGRST" m d _ _p))))) =
|
headers (PgError _ (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError (SQL.ServerError "PGRST" m d _ _p))))) =
|
||||||
case (parseMessage m, parseDetails d) of
|
case parseRaisePGRST m d of
|
||||||
(Just _, Just r) -> headers PGRSTParseError ++ map intoHeader (M.toList $ getHeaders r)
|
Right (_, r) -> map intoHeader (M.toList $ getHeaders r)
|
||||||
_ -> headers PGRSTParseError
|
Left e -> headers e
|
||||||
where
|
where
|
||||||
intoHeader (k,v) = (CI.mk $ T.encodeUtf8 k, T.encodeUtf8 v)
|
intoHeader (k,v) = (CI.mk $ T.encodeUtf8 k, T.encodeUtf8 v)
|
||||||
|
|
||||||
@@ -426,13 +441,13 @@ instance JSON.ToJSON SQL.QueryError where
|
|||||||
instance JSON.ToJSON SQL.CommandError where
|
instance JSON.ToJSON SQL.CommandError where
|
||||||
-- Special error raised with code PGRST, to allow full response control
|
-- Special error raised with code PGRST, to allow full response control
|
||||||
toJSON (SQL.ResultError (SQL.ServerError "PGRST" m d _ _p)) =
|
toJSON (SQL.ResultError (SQL.ServerError "PGRST" m d _ _p)) =
|
||||||
case (parseMessage m, parseDetails d) of
|
case parseRaisePGRST m d of
|
||||||
(Just r, Just _) -> JSON.object [
|
Right (r, _) -> JSON.object [
|
||||||
"code" .= getCode r,
|
"code" .= getCode r,
|
||||||
"message" .= getMessage r,
|
"message" .= getMessage r,
|
||||||
"details" .= checkMaybe (getDetails r),
|
"details" .= checkMaybe (getDetails r),
|
||||||
"hint" .= checkMaybe (getHint r)]
|
"hint" .= checkMaybe (getHint r)]
|
||||||
_ -> JSON.toJSON PGRSTParseError
|
Left e -> JSON.toJSON e
|
||||||
where
|
where
|
||||||
checkMaybe = maybe JSON.Null JSON.String
|
checkMaybe = maybe JSON.Null JSON.String
|
||||||
|
|
||||||
@@ -493,9 +508,9 @@ pgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError
|
|||||||
"42501" -> if authed then HTTP.status403 else HTTP.status401 -- insufficient privilege
|
"42501" -> if authed then HTTP.status403 else HTTP.status401 -- insufficient privilege
|
||||||
'P':'T':n -> fromMaybe HTTP.status500 (HTTP.mkStatus <$> readMaybe n <*> pure m)
|
'P':'T':n -> fromMaybe HTTP.status500 (HTTP.mkStatus <$> readMaybe n <*> pure m)
|
||||||
"PGRST" ->
|
"PGRST" ->
|
||||||
case (parseMessage m, parseDetails d) of
|
case parseRaisePGRST m d of
|
||||||
(Just _, Just r) -> maybe (toEnum $ getStatus r) (HTTP.mkStatus (getStatus r) . T.encodeUtf8) (getStatusText r)
|
Right (_, r) -> maybe (toEnum $ getStatus r) (HTTP.mkStatus (getStatus r) . T.encodeUtf8) (getStatusText r)
|
||||||
_ -> status PGRSTParseError
|
Left e -> status e
|
||||||
_ -> HTTP.status400
|
_ -> HTTP.status400
|
||||||
|
|
||||||
_ -> HTTP.status500
|
_ -> HTTP.status500
|
||||||
@@ -579,11 +594,12 @@ instance JSON.FromJSON PgRaiseErrDetails where
|
|||||||
|
|
||||||
parseJSON _ = mzero
|
parseJSON _ = mzero
|
||||||
|
|
||||||
parseMessage :: ByteString -> Maybe PgRaiseErrMessage
|
parseRaisePGRST :: ByteString -> Maybe ByteString -> Either ApiRequestError (PgRaiseErrMessage, PgRaiseErrDetails)
|
||||||
parseMessage = JSON.decodeStrict
|
parseRaisePGRST m d = do
|
||||||
|
msgJson <- maybeToRight (PGRSTParseError $ MsgParseError m) (JSON.decodeStrict m)
|
||||||
parseDetails :: Maybe ByteString -> Maybe PgRaiseErrDetails
|
det <- maybeToRight (PGRSTParseError NoDetail) d
|
||||||
parseDetails d = JSON.decodeStrict =<< d
|
detJson <- maybeToRight (PGRSTParseError $ DetParseError det) (JSON.decodeStrict det)
|
||||||
|
return (msgJson, detJson)
|
||||||
|
|
||||||
-- Error codes are grouped by common modules or characteristics
|
-- Error codes are grouped by common modules or characteristics
|
||||||
data ErrorCode
|
data ErrorCode
|
||||||
|
|||||||
@@ -1452,17 +1452,29 @@ spec actualPgVersion =
|
|||||||
resHeaders `shouldSatisfy` elem ("X-Header", "str")
|
resHeaders `shouldSatisfy` elem ("X-Header", "str")
|
||||||
resBody `shouldBe` [json|{"code":"123","message":"ABC","details":null,"hint":null}|]
|
resBody `shouldBe` [json|{"code":"123","message":"ABC","details":null,"hint":null}|]
|
||||||
|
|
||||||
it "returns error for invalid JSON in RAISE Message field" $
|
it "returns error for invalid JSON in the MESSAGE option of the RAISE statement" $
|
||||||
get "/rpc/raise_sqlstate_invalid_json_message" `shouldRespondWith`
|
get "/rpc/raise_sqlstate_invalid_json_message" `shouldRespondWith`
|
||||||
[json|{"code":"PGRST121","message":"The message and detail field of RAISE 'PGRST' error expects JSON","details":null,"hint":null}|]
|
[json|{
|
||||||
|
"code":"PGRST121",
|
||||||
|
"message":"Could not parse JSON in the \"RAISE SQLSTATE 'PGRST'\" error",
|
||||||
|
"details":"Invalid JSON value for MESSAGE: 'INVALID'",
|
||||||
|
"hint":"MESSAGE must be a JSON object with obligatory keys: 'code', 'message' and optional keys: 'details', 'hint'."}|]
|
||||||
{ matchStatus = 500 }
|
{ matchStatus = 500 }
|
||||||
|
|
||||||
it "returns error for invalid JSON in RAISE Details field" $
|
it "returns error for invalid JSON in the DETAIL option of the RAISE statement" $
|
||||||
get "/rpc/raise_sqlstate_invalid_json_details" `shouldRespondWith`
|
get "/rpc/raise_sqlstate_invalid_json_details" `shouldRespondWith`
|
||||||
[json|{"code":"PGRST121","message":"The message and detail field of RAISE 'PGRST' error expects JSON","details":null,"hint":null}|]
|
[json|{
|
||||||
|
"code":"PGRST121",
|
||||||
|
"message":"Could not parse JSON in the \"RAISE SQLSTATE 'PGRST'\" error",
|
||||||
|
"details":"Invalid JSON value for DETAIL: 'INVALID'",
|
||||||
|
"hint":"DETAIL must be a JSON object with obligatory keys: 'status', 'headers' and optional key: 'status_text'."}|]
|
||||||
{ matchStatus = 500 }
|
{ matchStatus = 500 }
|
||||||
|
|
||||||
it "returns error for missing Details field in RAISE" $
|
it "returns error for missing DETAIL option in the RAISE statement" $
|
||||||
get "/rpc/raise_sqlstate_missing_details" `shouldRespondWith`
|
get "/rpc/raise_sqlstate_missing_details" `shouldRespondWith`
|
||||||
[json|{"code":"PGRST121","message":"The message and detail field of RAISE 'PGRST' error expects JSON","details":null,"hint":null}|]
|
[json|{
|
||||||
|
"code":"PGRST121",
|
||||||
|
"message":"Could not parse JSON in the \"RAISE SQLSTATE 'PGRST'\" error",
|
||||||
|
"details":"DETAIL is missing in the RAISE statement",
|
||||||
|
"hint":"DETAIL must be a JSON object with obligatory keys: 'status', 'headers' and optional key: 'status_text'."}|]
|
||||||
{ matchStatus = 500 }
|
{ matchStatus = 500 }
|
||||||
|
|||||||
Reference in New Issue
Block a user