feat: add handling=strict/lenient for Prefer header

This commit is contained in:
Taimoor Zaeem
2023-09-27 15:00:52 -03:00
committed by Steve Chavez
parent 2825ac059e
commit 3c1cdd434a
10 changed files with 148 additions and 31 deletions
+1
View File
@@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #2492, Allow full response control when raising exceptions - @taimoorzaeem, @laurenceisla - #2492, Allow full response control when raising exceptions - @taimoorzaeem, @laurenceisla
- #2771, Add `Server-Timing` header with JWT duration - @taimoorzaeem - #2771, Add `Server-Timing` header with JWT duration - @taimoorzaeem
- #2698, Add config `jwt-cache-max-lifetime` and implement JWT caching - @taimoorzaeem - #2698, Add config `jwt-cache-max-lifetime` and implement JWT caching - @taimoorzaeem
- #2943, Add `handling=strict/lenient` for Prefer header - @taimoorzaeem
### Fixed ### Fixed
+1
View File
@@ -215,6 +215,7 @@ test-suite spec
Feature.Query.PgSafeUpdateSpec Feature.Query.PgSafeUpdateSpec
Feature.Query.PlanSpec Feature.Query.PlanSpec
Feature.Query.PostGISSpec Feature.Query.PostGISSpec
Feature.Query.PreferencesSpec
Feature.Query.QueryLimitedSpec Feature.Query.QueryLimitedSpec
Feature.Query.QuerySpec Feature.Query.QuerySpec
Feature.Query.RangeSpec Feature.Query.RangeSpec
+37 -8
View File
@@ -15,6 +15,7 @@ module PostgREST.ApiRequest.Preferences
, PreferRepresentation(..) , PreferRepresentation(..)
, PreferResolution(..) , PreferResolution(..)
, PreferTransaction(..) , PreferTransaction(..)
, PreferHandling(..)
, fromHeaders , fromHeaders
, shouldCount , shouldCount
, prefAppliedHeader , prefAppliedHeader
@@ -26,7 +27,6 @@ import qualified Network.HTTP.Types.Header as HTTP
import Protolude import Protolude
-- $setup -- $setup
-- Setup for doctests -- Setup for doctests
-- >>> import Text.Pretty.Simple (pPrint) -- >>> import Text.Pretty.Simple (pPrint)
@@ -36,6 +36,7 @@ import Protolude
-- >>> deriving instance Show PreferCount -- >>> deriving instance Show PreferCount
-- >>> deriving instance Show PreferTransaction -- >>> deriving instance Show PreferTransaction
-- >>> deriving instance Show PreferMissing -- >>> deriving instance Show PreferMissing
-- >>> deriving instance Show PreferHandling
-- >>> deriving instance Show Preferences -- >>> deriving instance Show Preferences
-- | Preferences recognized by the application. -- | Preferences recognized by the application.
@@ -47,6 +48,8 @@ data Preferences
, preferCount :: Maybe PreferCount , preferCount :: Maybe PreferCount
, preferTransaction :: Maybe PreferTransaction , preferTransaction :: Maybe PreferTransaction
, preferMissing :: Maybe PreferMissing , preferMissing :: Maybe PreferMissing
, preferHandling :: Maybe PreferHandling
, invalidPrefs :: [ByteString]
} }
-- | -- |
@@ -62,11 +65,13 @@ data Preferences
-- , preferCount = Just ExactCount -- , preferCount = Just ExactCount
-- , preferTransaction = Nothing -- , preferTransaction = Nothing
-- , preferMissing = Nothing -- , preferMissing = Nothing
-- , preferHandling = Nothing
-- , invalidPrefs = []
-- } -- }
-- --
-- Multiple headers can also be used: -- Multiple headers can also be used:
-- --
-- >>> pPrint $ fromHeaders True [("Prefer", "resolution=ignore-duplicates"), ("Prefer", "count=exact"), ("Prefer", "missing=null")] -- >>> pPrint $ fromHeaders True [("Prefer", "resolution=ignore-duplicates"), ("Prefer", "count=exact"), ("Prefer", "missing=null"), ("Prefer", "handling=lenient"), ("Prefer", "invalid")]
-- Preferences -- Preferences
-- { preferResolution = Just IgnoreDuplicates -- { preferResolution = Just IgnoreDuplicates
-- , preferRepresentation = Nothing -- , preferRepresentation = Nothing
@@ -74,6 +79,8 @@ data Preferences
-- , preferCount = Just ExactCount -- , preferCount = Just ExactCount
-- , preferTransaction = Nothing -- , preferTransaction = Nothing
-- , preferMissing = Just ApplyNulls -- , preferMissing = Just ApplyNulls
-- , preferHandling = Just Lenient
-- , invalidPrefs = [ "invalid" ]
-- } -- }
-- --
-- If a preference is set more than once, only the first is used: -- If a preference is set more than once, only the first is used:
@@ -91,14 +98,10 @@ data Preferences
-- :} -- :}
-- Just IgnoreDuplicates -- Just IgnoreDuplicates
-- --
-- Preferences not recognized by the application are ignored:
--
-- >>> preferResolution $ fromHeaders True [("Prefer", "resolution=foo")]
-- Nothing
-- --
-- Preferences can be separated by arbitrary amounts of space, lower-case header is also recognized: -- Preferences can be separated by arbitrary amounts of space, lower-case header is also recognized:
-- --
-- >>> pPrint $ fromHeaders True [("prefer", "count=exact, tx=commit ,return=representation , missing=default")] -- >>> pPrint $ fromHeaders True [("prefer", "count=exact, tx=commit ,return=representation , missing=default, handling=strict, anything")]
-- Preferences -- Preferences
-- { preferResolution = Nothing -- { preferResolution = Nothing
-- , preferRepresentation = Just Full -- , preferRepresentation = Just Full
@@ -106,6 +109,8 @@ data Preferences
-- , preferCount = Just ExactCount -- , preferCount = Just ExactCount
-- , preferTransaction = Just Commit -- , preferTransaction = Just Commit
-- , preferMissing = Just ApplyDefaults -- , preferMissing = Just ApplyDefaults
-- , preferHandling = Just Strict
-- , invalidPrefs = [ "anything" ]
-- } -- }
-- --
fromHeaders :: Bool -> [HTTP.Header] -> Preferences fromHeaders :: Bool -> [HTTP.Header] -> Preferences
@@ -117,8 +122,20 @@ fromHeaders allowTxEndOverride headers =
, preferCount = parsePrefs [ExactCount, PlannedCount, EstimatedCount] , preferCount = parsePrefs [ExactCount, PlannedCount, EstimatedCount]
, preferTransaction = if allowTxEndOverride then parsePrefs [Commit, Rollback] else Nothing , preferTransaction = if allowTxEndOverride then parsePrefs [Commit, Rollback] else Nothing
, preferMissing = parsePrefs [ApplyDefaults, ApplyNulls] , preferMissing = parsePrefs [ApplyDefaults, ApplyNulls]
, preferHandling = parsePrefs [Strict, Lenient]
, invalidPrefs = filter (`notElem` acceptedPrefs) prefs
} }
where where
mapToHeadVal :: ToHeaderValue a => [a] -> [ByteString]
mapToHeadVal = map toHeaderValue
acceptedPrefs = mapToHeadVal [MergeDuplicates, IgnoreDuplicates] ++
mapToHeadVal [Full, None, HeadersOnly] ++
mapToHeadVal [SingleObject] ++
mapToHeadVal [ExactCount, PlannedCount, EstimatedCount] ++
mapToHeadVal [Commit, Rollback] ++
mapToHeadVal [ApplyDefaults, ApplyNulls] ++
mapToHeadVal [Strict, Lenient]
prefHeaders = filter ((==) HTTP.hPrefer . fst) headers prefHeaders = filter ((==) HTTP.hPrefer . fst) headers
prefs = fmap BS.strip . concatMap (BS.split ',' . snd) $ prefHeaders prefs = fmap BS.strip . concatMap (BS.split ',' . snd) $ prefHeaders
@@ -130,7 +147,7 @@ fromHeaders allowTxEndOverride headers =
prefMap = Map.fromList . fmap (\pref -> (toHeaderValue pref, pref)) prefMap = Map.fromList . fmap (\pref -> (toHeaderValue pref, pref))
prefAppliedHeader :: Preferences -> Maybe HTTP.Header prefAppliedHeader :: Preferences -> Maybe HTTP.Header
prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferParameters, preferCount, preferTransaction, preferMissing } = prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferParameters, preferCount, preferTransaction, preferMissing, preferHandling } =
if null prefsVals if null prefsVals
then Nothing then Nothing
else Just (HTTP.hPreferenceApplied, combined) else Just (HTTP.hPreferenceApplied, combined)
@@ -143,6 +160,7 @@ prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferPar
, toHeaderValue <$> preferParameters , toHeaderValue <$> preferParameters
, toHeaderValue <$> preferCount , toHeaderValue <$> preferCount
, toHeaderValue <$> preferTransaction , toHeaderValue <$> preferTransaction
, toHeaderValue <$> preferHandling
] ]
-- | -- |
@@ -223,3 +241,14 @@ data PreferMissing
instance ToHeaderValue PreferMissing where instance ToHeaderValue PreferMissing where
toHeaderValue ApplyDefaults = "missing=default" toHeaderValue ApplyDefaults = "missing=default"
toHeaderValue ApplyNulls = "missing=null" toHeaderValue ApplyNulls = "missing=null"
-- |
-- Handling of unrecognised preferences
data PreferHandling
= Strict -- ^ Throw error on unrecognised preferences
| Lenient -- ^ Ignore unrecognised preferences
deriving Eq
instance ToHeaderValue PreferHandling where
toHeaderValue Strict = "handling=strict"
toHeaderValue Lenient = "handling=lenient"
+1
View File
@@ -71,6 +71,7 @@ data ApiRequestError
| MediaTypeError [ByteString] | MediaTypeError [ByteString]
| InvalidBody ByteString | InvalidBody ByteString
| InvalidFilters | InvalidFilters
| InvalidPreferences [ByteString]
| InvalidRange RangeError | InvalidRange RangeError
| InvalidRpcMethod ByteString | InvalidRpcMethod ByteString
| LimitNoOrderError | LimitNoOrderError
+8
View File
@@ -66,6 +66,7 @@ instance PgrstError ApiRequestError where
status MediaTypeError{} = HTTP.status415 status MediaTypeError{} = HTTP.status415
status InvalidBody{} = HTTP.status400 status InvalidBody{} = HTTP.status400
status InvalidFilters = HTTP.status405 status InvalidFilters = HTTP.status405
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 NotFound = HTTP.status404
@@ -172,6 +173,11 @@ instance JSON.ToJSON ApiRequestError where
"message" .= ("Bad operator on the '" <> target <> "' embedded resource":: Text), "message" .= ("Bad operator on the '" <> target <> "' embedded resource":: Text),
"details" .= ("Only is null or not is null filters are allowed on embedded resources":: Text), "details" .= ("Only is null or not is null filters are allowed on embedded resources":: Text),
"hint" .= JSON.Null] "hint" .= JSON.Null]
toJSON (InvalidPreferences prefs) = JSON.object [
"code" .= ApiRequestErrorCode22,
"message" .= ("Invalid preferences given with handling=strict" :: Text),
"details" .= T.decodeUtf8 ("Invalid preferences: " <> BS.intercalate ", " prefs),
"hint" .= JSON.Null]
toJSON (NoRelBetween parent child embedHint schema allRels) = JSON.object [ toJSON (NoRelBetween parent child embedHint schema allRels) = JSON.object [
"code" .= SchemaCacheErrorCode00, "code" .= SchemaCacheErrorCode00,
@@ -646,6 +652,7 @@ data ErrorCode
| ApiRequestErrorCode19 | ApiRequestErrorCode19
| ApiRequestErrorCode20 | ApiRequestErrorCode20
| ApiRequestErrorCode21 | ApiRequestErrorCode21
| ApiRequestErrorCode22
-- Schema Cache errors -- Schema Cache errors
| SchemaCacheErrorCode00 | SchemaCacheErrorCode00
| SchemaCacheErrorCode01 | SchemaCacheErrorCode01
@@ -693,6 +700,7 @@ buildErrorCode code = "PGRST" <> case code of
ApiRequestErrorCode19 -> "119" ApiRequestErrorCode19 -> "119"
ApiRequestErrorCode20 -> "120" ApiRequestErrorCode20 -> "120"
ApiRequestErrorCode21 -> "121" ApiRequestErrorCode21 -> "121"
ApiRequestErrorCode22 -> "122"
SchemaCacheErrorCode00 -> "200" SchemaCacheErrorCode00 -> "200"
SchemaCacheErrorCode01 -> "201" SchemaCacheErrorCode01 -> "201"
+15 -13
View File
@@ -121,35 +121,37 @@ data InspectPlan = InspectPlan {
} }
wrappedReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Either Error WrappedReadPlan wrappedReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Either Error WrappedReadPlan
wrappedReadPlan identifier conf sCache apiRequest = do wrappedReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} = do
rPlan <- readPlan identifier conf sCache apiRequest rPlan <- readPlan identifier conf sCache apiRequest
mediaType <- mapLeft ApiRequestError $ negotiateContent conf (iAction apiRequest) (iPathInfo apiRequest) (iAcceptMediaType apiRequest) mediaType <- mapLeft ApiRequestError $ negotiateContent conf iAction iPathInfo iAcceptMediaType
binField <- mapLeft ApiRequestError $ binaryField conf mediaType Nothing rPlan binField <- mapLeft ApiRequestError $ binaryField conf mediaType Nothing rPlan
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
return $ WrappedReadPlan rPlan SQL.Read (mediaToAggregate mediaType binField apiRequest) mediaType return $ WrappedReadPlan rPlan SQL.Read (mediaToAggregate mediaType binField apiRequest) mediaType
mutateReadPlan :: Mutation -> ApiRequest -> QualifiedIdentifier -> AppConfig -> SchemaCache -> Either Error MutateReadPlan mutateReadPlan :: Mutation -> ApiRequest -> QualifiedIdentifier -> AppConfig -> SchemaCache -> Either Error MutateReadPlan
mutateReadPlan mutation apiRequest identifier conf sCache = do mutateReadPlan mutation apiRequest@ApiRequest{iPreferences=Preferences{..},..} identifier conf sCache = do
rPlan <- readPlan identifier conf sCache apiRequest rPlan <- readPlan identifier conf sCache apiRequest
mPlan <- mutatePlan mutation identifier apiRequest sCache rPlan mPlan <- mutatePlan mutation identifier apiRequest sCache rPlan
mediaType <- mapLeft ApiRequestError $ negotiateContent conf (iAction apiRequest) (iPathInfo apiRequest) (iAcceptMediaType apiRequest) mediaType <- mapLeft ApiRequestError $ negotiateContent conf iAction iPathInfo iAcceptMediaType
binField <- mapLeft ApiRequestError $ binaryField conf mediaType Nothing rPlan binField <- mapLeft ApiRequestError $ binaryField conf mediaType Nothing rPlan
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
return $ MutateReadPlan rPlan mPlan SQL.Write (mediaToAggregate mediaType binField apiRequest) mediaType return $ MutateReadPlan rPlan mPlan SQL.Write (mediaToAggregate mediaType binField apiRequest) mediaType
callReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> InvokeMethod -> Either Error CallReadPlan callReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> InvokeMethod -> Either Error CallReadPlan
callReadPlan identifier conf sCache apiRequest invMethod = do callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} invMethod = do
let paramKeys = case invMethod of let paramKeys = case invMethod of
InvGet -> S.fromList $ fst <$> qsParams' InvGet -> S.fromList $ fst <$> qsParams'
InvHead -> S.fromList $ fst <$> qsParams' InvHead -> S.fromList $ fst <$> qsParams'
InvPost -> iColumns apiRequest InvPost -> iColumns
proc@Function{..} <- mapLeft ApiRequestError $ proc@Function{..} <- mapLeft ApiRequestError $
findProc identifier paramKeys (preferParameters == Just SingleObject) (dbRoutines sCache) (iContentMediaType apiRequest) (invMethod == InvPost) findProc identifier paramKeys (preferParameters == Just SingleObject) (dbRoutines sCache) iContentMediaType (invMethod == InvPost)
let relIdentifier = QualifiedIdentifier pdSchema (fromMaybe pdName $ Routine.funcTableName proc) -- done so a set returning function can embed other relations let relIdentifier = QualifiedIdentifier pdSchema (fromMaybe pdName $ Routine.funcTableName proc) -- done so a set returning function can embed other relations
rPlan <- readPlan relIdentifier conf sCache apiRequest rPlan <- readPlan relIdentifier conf sCache apiRequest
let args = case (invMethod, iContentMediaType apiRequest) of let args = case (invMethod, iContentMediaType) of
(InvGet, _) -> jsonRpcParams proc qsParams' (InvGet, _) -> jsonRpcParams proc qsParams'
(InvHead, _) -> jsonRpcParams proc qsParams' (InvHead, _) -> jsonRpcParams proc qsParams'
(InvPost, MTUrlEncoded) -> maybe mempty (jsonRpcParams proc . payArray) $ iPayload apiRequest (InvPost, MTUrlEncoded) -> maybe mempty (jsonRpcParams proc . payArray) iPayload
(InvPost, _) -> maybe mempty payRaw $ iPayload apiRequest (InvPost, _) -> maybe mempty payRaw iPayload
txMode = case (invMethod, pdVolatility) of txMode = case (invMethod, pdVolatility) of
(InvGet, _) -> SQL.Read (InvGet, _) -> SQL.Read
(InvHead, _) -> SQL.Read (InvHead, _) -> SQL.Read
@@ -157,12 +159,12 @@ callReadPlan identifier conf sCache apiRequest invMethod = do
(InvPost, Routine.Immutable) -> SQL.Read (InvPost, Routine.Immutable) -> SQL.Read
(InvPost, Routine.Volatile) -> SQL.Write (InvPost, Routine.Volatile) -> SQL.Write
cPlan = callPlan proc apiRequest paramKeys args rPlan cPlan = callPlan proc apiRequest paramKeys args rPlan
mediaType <- mapLeft ApiRequestError $ negotiateContent conf (iAction apiRequest) (iPathInfo apiRequest) (iAcceptMediaType apiRequest) mediaType <- mapLeft ApiRequestError $ negotiateContent conf iAction iPathInfo iAcceptMediaType
binField <- mapLeft ApiRequestError $ binaryField conf mediaType (Just proc) rPlan binField <- mapLeft ApiRequestError $ binaryField conf mediaType (Just proc) rPlan
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
return $ CallReadPlan rPlan cPlan txMode proc (mediaToAggregate mediaType binField apiRequest) mediaType return $ CallReadPlan rPlan cPlan txMode proc (mediaToAggregate mediaType binField apiRequest) mediaType
where where
Preferences{..} = iPreferences apiRequest qsParams' = QueryParams.qsParams iQueryParams
qsParams' = QueryParams.qsParams (iQueryParams apiRequest)
inspectPlan :: AppConfig -> ApiRequest -> Either Error InspectPlan inspectPlan :: AppConfig -> ApiRequest -> Either Error InspectPlan
inspectPlan conf apiRequest = do inspectPlan conf apiRequest = do
+6 -6
View File
@@ -86,7 +86,7 @@ readResponse WrappedReadPlan{wrMedia} headersOnly identifier ctxApiRequest@ApiRe
RSStandard{..} -> do RSStandard{..} -> do
let let
(status, contentRange) = RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal (status, contentRange) = RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing Nothing preferCount preferTransaction Nothing prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing Nothing preferCount preferTransaction Nothing preferHandling []
headers = headers =
[ contentRange [ contentRange
, ( "Content-Location" , ( "Content-Location"
@@ -118,7 +118,7 @@ createResponse QualifiedIdentifier{..} MutateReadPlan{mrMutatePlan, mrMedia} ctx
pkCols = case mrMutatePlan of { Insert{insPkCols} -> insPkCols; _ -> mempty;} pkCols = case mrMutatePlan of { Insert{insPkCols} -> insPkCols; _ -> mempty;}
prefHeader = prefAppliedHeader $ prefHeader = prefAppliedHeader $
Preferences (if null pkCols && isNothing (qsOnConflict iQueryParams) then Nothing else preferResolution) Preferences (if null pkCols && isNothing (qsOnConflict iQueryParams) then Nothing else preferResolution)
preferRepresentation Nothing preferCount preferTransaction preferMissing preferRepresentation Nothing preferCount preferTransaction preferMissing preferHandling []
headers = headers =
catMaybes catMaybes
[ if null rsLocation then [ if null rsLocation then
@@ -155,7 +155,7 @@ updateResponse MutateReadPlan{mrMedia} ctxApiRequest@ApiRequest{iPreferences=Pre
contentRangeHeader = contentRangeHeader =
Just . RangeQuery.contentRangeH 0 (rsQueryTotal - 1) $ Just . RangeQuery.contentRangeH 0 (rsQueryTotal - 1) $
if shouldCount preferCount then Just rsQueryTotal else Nothing if shouldCount preferCount then Just rsQueryTotal else Nothing
prefHeader = prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction preferMissing prefHeader = prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction preferMissing preferHandling []
headers = catMaybes [contentRangeHeader, prefHeader] ++ serverTimingHeader serverTimingParams headers = catMaybes [contentRangeHeader, prefHeader] ++ serverTimingHeader serverTimingParams
let let
@@ -175,7 +175,7 @@ singleUpsertResponse :: MutateReadPlan -> ApiRequest -> ResultSet -> Maybe Serve
singleUpsertResponse MutateReadPlan{mrMedia} ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} resultSet serverTimingParams = case resultSet of singleUpsertResponse MutateReadPlan{mrMedia} ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} resultSet serverTimingParams = case resultSet of
RSStandard {..} -> do RSStandard {..} -> do
let let
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction Nothing prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction Nothing preferHandling []
sTHeader = serverTimingHeader serverTimingParams sTHeader = serverTimingHeader serverTimingParams
cTHeader = contentTypeHeaders mrMedia ctxApiRequest cTHeader = contentTypeHeaders mrMedia ctxApiRequest
@@ -198,7 +198,7 @@ deleteResponse MutateReadPlan{mrMedia} ctxApiRequest@ApiRequest{iPreferences=Pre
contentRangeHeader = contentRangeHeader =
RangeQuery.contentRangeH 1 0 $ RangeQuery.contentRangeH 1 0 $
if shouldCount preferCount then Just rsQueryTotal else Nothing if shouldCount preferCount then Just rsQueryTotal else Nothing
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction Nothing prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction Nothing preferHandling []
headers = contentRangeHeader : prefHeader ++ serverTimingHeader serverTimingParams headers = contentRangeHeader : prefHeader ++ serverTimingHeader serverTimingParams
let (status, headers', body) = let (status, headers', body) =
@@ -251,7 +251,7 @@ invokeResponse CallReadPlan{crMedia} invMethod proc ctxApiRequest@ApiRequest{iPr
then Error.errorPayload $ Error.ApiRequestError $ ApiRequestTypes.InvalidRange then Error.errorPayload $ Error.ApiRequestError $ ApiRequestTypes.InvalidRange
$ ApiRequestTypes.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal) $ ApiRequestTypes.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal)
else LBS.fromStrict rsBody else LBS.fromStrict rsBody
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing preferParameters preferCount preferTransaction Nothing prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing preferParameters preferCount preferTransaction Nothing preferHandling []
headers = contentRange : prefHeader ++ serverTimingHeader serverTimingParams headers = contentRange : prefHeader ++ serverTimingHeader serverTimingParams
let (status', headers', body) = let (status', headers', body) =
+4 -4
View File
@@ -1182,9 +1182,9 @@ def test_server_timing_jwt_should_not_decrease_when_caching_disabled(defaultenv)
first_dur = float(first_dur_text[8:]) # skip "jwt;dur=" first_dur = float(first_dur_text[8:]) # skip "jwt;dur="
second_dur = float(second_dur_text[8:]) second_dur = float(second_dur_text[8:])
# their difference should be less than 100 # their difference should be less than 150
# implying that token is not cached # implying that token is not cached
assert (first_dur - second_dur) < 100.0 assert (first_dur - second_dur) < 150.0
def test_jwt_cache_with_no_exp_claim(defaultenv): def test_jwt_cache_with_no_exp_claim(defaultenv):
@@ -1211,6 +1211,6 @@ def test_jwt_cache_with_no_exp_claim(defaultenv):
first_dur = float(first_dur_text[8:]) # skip "jwt;dur=" first_dur = float(first_dur_text[8:]) # skip "jwt;dur="
second_dur = float(second_dur_text[8:]) second_dur = float(second_dur_text[8:])
# their difference should be less than 100 # their difference should be atleast 300, implying
# implying that token is not cached # that JWT Caching is working as expected
assert (first_dur - second_dur) > 300.0 assert (first_dur - second_dur) > 300.0
@@ -0,0 +1,73 @@
module Feature.Query.PreferencesSpec where
import Network.Wai (Application)
import Network.HTTP.Types
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Protolude hiding (get)
import SpecHelper
spec :: SpecWith ((), Application)
spec =
describe "check prefer: handling=strict and handling=lenient" $ do
context "check behaviour of Prefer: handling=strict" $ do
it "throws error when handling=strict and invalid prefs are given" $
request methodGet "/items" [("Prefer", "handling=strict, anything")] ""
`shouldRespondWith`
[json|{"details":"Invalid preferences: anything","message":"Invalid preferences given with handling=strict","code":"PGRST122","hint":null}|]
{ matchStatus = 400 }
it "throw error when handling=strict and invalid prefs are given with multiples in separate prefers" $
request methodGet "/items" [("Prefer", "handling=strict"),("Prefer","something, else")] ""
`shouldRespondWith`
[json|{"details":"Invalid preferences: something, else","message":"Invalid preferences given with handling=strict","code":"PGRST122","hint":null}|]
{ matchStatus = 400 }
it "throws error with post request" $
request methodPost "/organizations?select=*"
[("Prefer","return=representation, handling=strict, anything")]
[json|{"id":7,"name":"John","referee":null,"auditor":null,"manager_id":6}|]
`shouldRespondWith`
[json|{"details":"Invalid preferences: anything","message":"Invalid preferences given with handling=strict","code":"PGRST122","hint":null}|]
{ matchStatus = 400 }
it "throws error with rpc" $
request methodPost "/rpc/overloaded_unnamed_param"
[("Content-Type", "application/json"), ("Prefer", "handling=strict, anything")]
[json|{}|]
`shouldRespondWith`
[json|{"details":"Invalid preferences: anything","message":"Invalid preferences given with handling=strict","code":"PGRST122","hint":null}|]
{ matchStatus = 400 }
context "check behaviour of Prefer: handling=lenient" $ do
it "does not throw error when handling=lenient and invalid prefs" $
request methodGet "/items" [("Prefer", "handling=lenient, anything")] ""
`shouldRespondWith` 200
it "does not throw error when handling=lenient and invalid prefs in multiples prefers" $
request methodGet "/items" [("Prefer", "handling=lenient"), ("Prefer", "anything")] ""
`shouldRespondWith` 200
it "does not throw error with post request" $
request methodPost "/organizations?select=*"
[("Prefer","return=representation, handling=lenient, anything")]
[json|{"id":7,"name":"John","referee":null,"auditor":null,"manager_id":6}|]
`shouldRespondWith`
[json|[{"id":7,"name":"John","referee":null,"auditor":null,"manager_id":6}]|]
{ matchStatus = 201
, matchHeaders = [ matchContentTypeJson ]
}
it "does not throw error with rpc" $
request methodPost "/rpc/overloaded_unnamed_param"
[("Content-Type", "application/json"), ("Prefer", "handling=lenient, anything")]
[json|{}|]
`shouldRespondWith`
[json| 1 |]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
+2
View File
@@ -49,6 +49,7 @@ import qualified Feature.Query.NullsStripSpec
import qualified Feature.Query.PgSafeUpdateSpec import qualified Feature.Query.PgSafeUpdateSpec
import qualified Feature.Query.PlanSpec import qualified Feature.Query.PlanSpec
import qualified Feature.Query.PostGISSpec import qualified Feature.Query.PostGISSpec
import qualified Feature.Query.PreferencesSpec
import qualified Feature.Query.QueryLimitedSpec import qualified Feature.Query.QueryLimitedSpec
import qualified Feature.Query.QuerySpec import qualified Feature.Query.QuerySpec
import qualified Feature.Query.RangeSpec import qualified Feature.Query.RangeSpec
@@ -138,6 +139,7 @@ main = do
, ("Feature.OptionsSpec" , Feature.OptionsSpec.spec actualPgVersion) , ("Feature.OptionsSpec" , Feature.OptionsSpec.spec actualPgVersion)
, ("Feature.Query.PgSafeUpdateSpec.disabledSpec" , Feature.Query.PgSafeUpdateSpec.disabledSpec) , ("Feature.Query.PgSafeUpdateSpec.disabledSpec" , Feature.Query.PgSafeUpdateSpec.disabledSpec)
, ("Feature.Query.PlanSpec.disabledSpec" , Feature.Query.PlanSpec.disabledSpec) , ("Feature.Query.PlanSpec.disabledSpec" , Feature.Query.PlanSpec.disabledSpec)
, ("Feature.Query.PreferencesSpec" , Feature.Query.PreferencesSpec.spec)
, ("Feature.Query.QuerySpec" , Feature.Query.QuerySpec.spec actualPgVersion) , ("Feature.Query.QuerySpec" , Feature.Query.QuerySpec.spec actualPgVersion)
, ("Feature.Query.RawOutputTypesSpec" , Feature.Query.RawOutputTypesSpec.spec) , ("Feature.Query.RawOutputTypesSpec" , Feature.Query.RawOutputTypesSpec.spec)
, ("Feature.Query.RpcSpec" , Feature.Query.RpcSpec.spec actualPgVersion) , ("Feature.Query.RpcSpec" , Feature.Query.RpcSpec.spec actualPgVersion)