Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ All notable changes to this project will be documented in this file. From versio

## [14.1] - 2025-11-05

- Fix regression where the `PGRST103` error response was truncated by @laurenceisla in #4455
+ Happened when an `offset` was greater than the rows requested and `Prefer: count=exact` was sent.
- Fix not returning `Content-Length` on empty HTTP `201` responses by @laurenceisla in #4518

## Fixed

- Fix `db-pre-config` function failing when function names are pg reserved words by @taimoorzaeem in #4380
Expand Down
35 changes: 16 additions & 19 deletions src/PostgREST/Response.hs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ actionResponse :: DbResult -> ApiRequest -> (Text, Text) -> AppConfig -> SchemaC
actionResponse (DbCrudResult WrappedReadPlan{pMedia, wrHdrsOnly=headersOnly, crudQi=identifier} RSStandard{..}) ctxApiRequest@ApiRequest{iPreferences=Preferences{..},..} _ _ _ _ _ = do
let
(status, contentRange) = RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal
cLHeader = if headersOnly then mempty else [contentLengthHeaderStrict rsBody]
cLHeader = if headersOnly then mempty else [ contentLengthHeader bod ]
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing preferCount preferTransaction Nothing preferHandling preferTimezone Nothing []
headers =
[ contentRange
Expand Down Expand Up @@ -106,7 +106,6 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationCreate, mrMutateP
)
, Just . RangeQuery.contentRangeH 1 0 $
if shouldCount preferCount then Just rsQueryTotal else Nothing
, Just $ contentLengthHeaderStrict rsBody
, prefHeader ]

isInsertIfGTZero i =
Expand All @@ -121,7 +120,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationCreate, mrMutateP
Just HeadersOnly -> (headers, mempty)
Nothing -> (headers, mempty)

(ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status headers'
(ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status $ contentLengthHeader bod:headers'

Right $ PgrstResponse ovStatus ovHeaders bod

Expand All @@ -132,10 +131,11 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationUpdate, pMedia} R
if shouldCount preferCount then Just rsQueryTotal else Nothing
prefHeader = prefAppliedHeader $ Preferences Nothing preferRepresentation preferCount preferTransaction preferMissing preferHandling preferTimezone preferMaxAffected []
headers = catMaybes [contentRangeHeader, prefHeader]
lbsBody = LBS.fromStrict rsBody

let (status, headers', body) =
case preferRepresentation of
Just Full -> (HTTP.status200, headers ++ [contentLengthHeaderStrict rsBody] ++ contentTypeHeaders pMedia ctxApiRequest, LBS.fromStrict rsBody)
Just Full -> (HTTP.status200, headers ++ [contentLengthHeader lbsBody] ++ contentTypeHeaders pMedia ctxApiRequest, lbsBody)
Just None -> (HTTP.status204, headers, mempty)
_ -> (HTTP.status204, headers, mempty)

Expand All @@ -146,14 +146,15 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationUpdate, pMedia} R
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationSingleUpsert, pMedia} RSStandard{..}) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = do
let
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation preferCount preferTransaction Nothing preferHandling preferTimezone Nothing []
cLHeader = [contentLengthHeaderStrict rsBody]
lbsBody = LBS.fromStrict rsBody
cLHeader = [contentLengthHeader lbsBody]
cTHeader = contentTypeHeaders pMedia ctxApiRequest

let isInsertIfGTZero i = if i > 0 then HTTP.status201 else HTTP.status200
upsertStatus = isInsertIfGTZero $ fromJust rsInserted
(status, headers, body) =
case preferRepresentation of
Just Full -> (upsertStatus, cLHeader ++ cTHeader ++ prefHeader, LBS.fromStrict rsBody)
Just Full -> (upsertStatus, cLHeader ++ cTHeader ++ prefHeader, lbsBody)
Just None -> (HTTP.status204, prefHeader, mempty)
_ -> (HTTP.status204, prefHeader, mempty)
(ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status headers
Expand All @@ -165,9 +166,10 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationDelete, pMedia} R
contentRangeHeader = RangeQuery.contentRangeH 1 0 $ if shouldCount preferCount then Just rsQueryTotal else Nothing
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation preferCount preferTransaction Nothing preferHandling preferTimezone preferMaxAffected []
headers = contentRangeHeader : prefHeader
lbsBody = LBS.fromStrict rsBody
(status, headers', body) =
case preferRepresentation of
Just Full -> (HTTP.status200, headers ++ [contentLengthHeaderStrict rsBody] ++ contentTypeHeaders pMedia ctxApiRequest, LBS.fromStrict rsBody)
Just Full -> (HTTP.status200, headers ++ [contentLengthHeader lbsBody] ++ contentTypeHeaders pMedia ctxApiRequest, lbsBody)
Just None -> (HTTP.status204, headers, mempty)
_ -> (HTTP.status204, headers, mempty)

Expand All @@ -185,7 +187,7 @@ actionResponse (DbCrudResult CallReadPlan{pMedia, crInvMthd=invMethod, crProc=pr
else LBS.fromStrict rsBody
isHeadMethod = invMethod == InvRead True
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing preferCount preferTransaction Nothing preferHandling preferTimezone preferMaxAffected []
cLHeader = if isHeadMethod then mempty else [contentLengthHeaderLazy rsOrErrBody]
cLHeader = if isHeadMethod then mempty else [contentLengthHeader rsOrErrBody]
headers = contentRange : prefHeader
(status', headers', body) =
if Routine.funcReturnsVoid proc then
Expand All @@ -200,12 +202,13 @@ actionResponse (DbCrudResult CallReadPlan{pMedia, crInvMthd=invMethod, crProc=pr
Right $ PgrstResponse ovStatus ovHeaders body

actionResponse (DbPlanResult media plan) ctxApiRequest _ _ _ _ _ =
Right $ PgrstResponse HTTP.status200 (contentLengthHeaderStrict plan : contentTypeHeaders media ctxApiRequest) $ LBS.fromStrict plan
let body = LBS.fromStrict plan in
Right $ PgrstResponse HTTP.status200 (contentLengthHeader body : contentTypeHeaders media ctxApiRequest) body

actionResponse (MaybeDbResult InspectPlan{ipHdrsOnly=headersOnly} body) _ versions conf sCache schema negotiatedByProfile =
let
rsBody = maybe mempty (\(x, y, z) -> if headersOnly then mempty else OpenAPI.encode versions conf sCache x y z) body
cLHeader = if headersOnly then mempty else [contentLengthHeaderLazy rsBody]
cLHeader = if headersOnly then mempty else [contentLengthHeader rsBody]
in
Right $ PgrstResponse HTTP.status200 (MediaType.toContentType MTOpenAPI : cLHeader ++ maybeToList (profileHeader schema negotiatedByProfile)) rsBody

Expand All @@ -232,7 +235,7 @@ actionResponse (NoDbResult SchemaInfoPlan) _ _ _ _ _ _ = respondInfo "OPTIONS,GE
respondInfo :: ByteString -> Either Error.Error PgrstResponse
respondInfo allowHeader =
let allOrigins = ("Access-Control-Allow-Origin", "*") in
Right $ PgrstResponse HTTP.status200 [contentLengthHeaderStrict mempty, allOrigins, (HTTP.hAllow, allowHeader)] mempty
Right $ PgrstResponse HTTP.status200 [contentLengthHeader mempty, allOrigins, (HTTP.hAllow, allowHeader)] mempty

-- Status and headers can be overridden as per https://postgrest.org/en/stable/references/transactions.html#response-headers
overrideStatusHeaders :: Maybe Text -> Maybe BS.ByteString -> HTTP.Status -> [HTTP.Header]-> Either Error.Error (HTTP.Status, [HTTP.Header])
Expand All @@ -249,14 +252,8 @@ decodeGucStatus :: Maybe Text -> Either Error.Error (Maybe HTTP.Status)
decodeGucStatus =
maybe (Right Nothing) $ first (const . Error.ApiRequestError $ Error.GucStatusError) . fmap (Just . toEnum . fst) . decimal

contentLengthHeader :: Show b => (a -> b) -> a -> HTTP.Header
contentLengthHeader lenFn body = ("Content-Length", show (lenFn body))

contentLengthHeaderStrict :: BS.ByteString -> HTTP.Header
contentLengthHeaderStrict = contentLengthHeader BS.length

contentLengthHeaderLazy :: LBS.ByteString -> HTTP.Header
contentLengthHeaderLazy = contentLengthHeader LBS.length
contentLengthHeader :: LBS.ByteString -> HTTP.Header
contentLengthHeader body = ("Content-Length", show (LBS.length body))

contentTypeHeaders :: MediaType -> ApiRequest -> [HTTP.Header]
contentTypeHeaders mediaType ApiRequest{..} =
Expand Down
18 changes: 15 additions & 3 deletions test/spec/Feature/Query/InsertSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ spec actualPgVersion = do
, matchHeaders = [ matchHeaderAbsent hContentType
, "Location" <:> "/projects?id=eq.11"
, "Content-Range" <:> "*/*"
, "Content-Length" <:> "0"
, "Preference-Applied" <:> "return=headers-only"]
}

Expand All @@ -151,6 +152,7 @@ spec actualPgVersion = do
, matchHeaders = [ matchHeaderAbsent hContentType
, "Location" <:> "/car_models?name=eq.Enzo&year=eq.2021"
, "Content-Range" <:> "*/*"
, "Content-Length" <:> "0"
, "Preference-Applied" <:> "return=headers-only"]
}

Expand All @@ -163,7 +165,8 @@ spec actualPgVersion = do
""
{ matchStatus = 201
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hLocation ]
, matchHeaderAbsent hLocation
, "Content-Length" <:> "0"]
}

context "from an html form" $
Expand All @@ -175,7 +178,8 @@ spec actualPgVersion = do
`shouldRespondWith`
""
{ matchStatus = 201
, matchHeaders = [ matchHeaderAbsent hContentType ]
, matchHeaders = [ matchHeaderAbsent hContentType
, "Content-Length" <:> "0"]
}

context "with no pk supplied" $ do
Expand All @@ -199,6 +203,7 @@ spec actualPgVersion = do
""
{ matchStatus = 201
, matchHeaders = [ matchHeaderAbsent hContentType
, "Content-Length" <:> "0"
, "Location" <:> "/auto_incrementing_pk?id=eq.2"
, "Preference-Applied" <:> "return=headers-only"]
}
Expand Down Expand Up @@ -741,6 +746,7 @@ spec actualPgVersion = do
""
{ matchStatus = 201
, matchHeaders = [matchHeaderAbsent hContentType
, "Content-Length" <:> "0"
, "Preference-Applied" <:> "return=minimal"]
}

Expand All @@ -753,7 +759,8 @@ spec actualPgVersion = do
""
{ matchStatus = 201
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hLocation ]
, matchHeaderAbsent hLocation
, "Content-Length" <:> "0"]
}

it "returns a location header with pks from both tables" $
Expand All @@ -765,6 +772,7 @@ spec actualPgVersion = do
, matchHeaders = [ matchHeaderAbsent hContentType
, "Location" <:> "/with_multiple_pks?pk1=eq.1&pk2=eq.2"
, "Content-Range" <:> "*/*"
, "Content-Length" <:> "0"
, "Preference-Applied" <:> "return=headers-only"]
}

Expand All @@ -778,6 +786,7 @@ spec actualPgVersion = do
, matchHeaders = [ matchHeaderAbsent hContentType
, "Location" <:> "/compound_pk_view?k1=eq.1&k2=eq.test"
, "Content-Range" <:> "*/*"
, "Content-Length" <:> "0"
, "Preference-Applied" <:> "return=headers-only"]
}

Expand All @@ -790,6 +799,7 @@ spec actualPgVersion = do
, matchHeaders = [ matchHeaderAbsent hContentType
, "Location" <:> "/test_null_pk_competitors_sponsors?id=eq.1&sponsor_id=is.null"
, "Content-Range" <:> "*/*"
, "Content-Length" <:> "0"
, "Preference-Applied" <:> "return=headers-only"]
}

Expand All @@ -807,6 +817,7 @@ spec actualPgVersion = do
, matchHeaders = [ matchHeaderAbsent hContentType
, "Location" <:> "/datarep_todos?id=eq.5"
, "Content-Range" <:> "*/*"
, "Content-Length" <:> "0"
, "Preference-Applied" <:> "return=headers-only"]
}

Expand Down Expand Up @@ -862,6 +873,7 @@ spec actualPgVersion = do
, matchHeaders = [ matchHeaderAbsent hContentType
, "Location" <:> "/datarep_todos_computed?id=eq.5"
, "Content-Range" <:> "*/*"
, "Content-Length" <:> "0"
, "Preference-Applied" <:> "return=headers-only"]
}

Expand Down
9 changes: 6 additions & 3 deletions test/spec/Feature/Query/RangeSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ spec = do
"hint":null
}|]
{ matchStatus = 416
, matchHeaders = ["Content-Range" <:> "*/0"]
, matchHeaders = [ "Content-Range" <:> "*/0"
, "Content-Length" <:> "144"]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test only responds with the Content-Length when it's added to matchHeaders... this is unexpected for a test since it shouldn't modify how our application works. Unless there's a way to make this same request and avoid getting the "Content-Length" or ignore it?

}

it "refuses a range requesting start past last item" $
Expand Down Expand Up @@ -288,7 +289,8 @@ spec = do
"hint":null
}|]
{ matchStatus = 416
, matchHeaders = ["Content-Range" <:> "*/0"]
, matchHeaders = [ "Content-Range" <:> "*/0"
, "Content-Length" <:> "144"]
}

it "refuses a range requesting start past last item" $
Expand Down Expand Up @@ -470,7 +472,8 @@ spec = do
"hint":null
}|]
{ matchStatus = 416
, matchHeaders = ["Content-Range" <:> "*/0"]
, matchHeaders = [ "Content-Range" <:> "*/0"
, "Content-Length" <:> "144"]
}

it "refuses a range requesting start past last item" $
Expand Down
4 changes: 3 additions & 1 deletion test/spec/Feature/RollbackSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ postItem =
`shouldRespondWith`
""
{ matchStatus = 201
, matchHeaders = [matchHeaderAbsent hContentType] }
, matchHeaders = [ matchHeaderAbsent hContentType
, "Content-Length" <:> "0" ]
}

-- removes Items left over from POST, PUT, and PATCH
deleteItems =
Expand Down