Skip to content

Commit 073c717

Browse files
Qu4trosteve-chavez
authored andcommitted
Fix wrong status 404 when PATCH request didn't change anything (PostgREST#1272)
1 parent 4db69eb commit 073c717

File tree

5 files changed

+117
-80
lines changed

5 files changed

+117
-80
lines changed

src/PostgREST/App.hs

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,8 @@ app dbStructure proc conf apiRequest =
162162
, if iPreferRepresentation apiRequest == Full
163163
then Just $ toHeader contentType
164164
else Nothing
165-
, Just . contentRangeH 1 0 $
166-
toInteger <$> if shouldCount then Just queryTotal else Nothing
165+
, Just $ contentRangeH 1 0 $
166+
if shouldCount then Just queryTotal else Nothing
167167
, if null pkCols
168168
then Nothing
169169
else (\x -> ("Preference-Applied", show x)) <$> iPreferResolution apiRequest
@@ -188,21 +188,28 @@ app dbStructure proc conf apiRequest =
188188
(iPreferRepresentation apiRequest) []
189189
row <- H.statement (toS $ pjRaw pJson) stm
190190
let (_, queryTotal, _, body) = extractQueryResult row
191-
if contentType == CTSingularJSON
192-
&& queryTotal /= 1
193-
&& iPreferRepresentation apiRequest == Full
194-
then do
195-
HT.condemn
196-
return $ singularityError (toInteger queryTotal)
197-
else do
198-
let r = contentRangeH 0 (toInteger $ queryTotal-1)
199-
(toInteger <$> if shouldCount then Just queryTotal else Nothing)
200-
s = if iPreferRepresentation apiRequest == Full
201-
then status200
202-
else status204
203-
return $ if iPreferRepresentation apiRequest == Full
204-
then responseLBS s [toHeader contentType, r] (toS body)
205-
else responseLBS s [r] ""
191+
192+
updateIsNoOp = S.null $ pjKeys pJson
193+
contentRangeHeader = contentRangeH 0 (queryTotal - 1) $
194+
if shouldCount then Just queryTotal else Nothing
195+
minimalHeaders = [contentRangeHeader]
196+
fullHeaders = toHeader contentType : minimalHeaders
197+
198+
status | queryTotal == 0 && not updateIsNoOp = status404
199+
| iPreferRepresentation apiRequest == Full = status200
200+
| otherwise = status204
201+
202+
case (contentType, iPreferRepresentation apiRequest) of
203+
(CTSingularJSON, Full)
204+
| queryTotal == 1 -> return $ responseLBS status fullHeaders (toS body)
205+
| otherwise -> HT.condemn >> return (singularityError queryTotal)
206+
207+
(_, Full) ->
208+
return $ responseLBS status fullHeaders (toS body)
209+
210+
(_, _) ->
211+
return $ responseLBS status minimalHeaders mempty
212+
206213

207214
(ActionSingleUpsert, TargetIdent (QualifiedIdentifier tSchema tName), Just ProcessedJSON{pjRaw, pjType, pjKeys}) ->
208215
case mutateSqlParts tSchema tName of
@@ -246,7 +253,7 @@ app dbStructure proc conf apiRequest =
246253
row <- H.statement mempty stm
247254
let (_, queryTotal, _, body) = extractQueryResult row
248255
r = contentRangeH 1 0 $
249-
toInteger <$> if shouldCount then Just queryTotal else Nothing
256+
if shouldCount then Just queryTotal else Nothing
250257
if contentType == CTSingularJSON
251258
&& queryTotal /= 1
252259
&& iPreferRepresentation apiRequest == Full
@@ -332,9 +339,10 @@ app dbStructure proc conf apiRequest =
332339
selectQuery = requestToQuery schema False <$> readDbRequest
333340
countQuery = requestToCountQuery schema <$> readDbRequest
334341
readSqlParts = (,) <$> selectQuery <*> countQuery
342+
mutationDbRequest s t = mutateRequest apiRequest t (tablePKCols dbStructure s t) =<< fldNames
335343
mutateSqlParts s t =
336344
(,) <$> selectQuery
337-
<*> (requestToQuery schema False . DbMutate <$> (mutateRequest apiRequest t (tablePKCols dbStructure s t) =<< fldNames))
345+
<*> (requestToQuery schema False . DbMutate <$> mutationDbRequest s t)
338346

339347
responseContentTypeOrError :: [ContentType] -> Action -> Either Response ContentType
340348
responseContentTypeOrError accepts action = serves contentTypesForRequest accepts
@@ -381,7 +389,7 @@ rangeStatus lower upper (Just total)
381389
| (1 + upper - lower) < total = status206
382390
| otherwise = status200
383391

384-
contentRangeH :: Integer -> Integer -> Maybe Integer -> Header
392+
contentRangeH :: (Integral a, Show a) => a -> a -> Maybe a -> Header
385393
contentRangeH lower upper total =
386394
("Content-Range", headerValue)
387395
where

src/PostgREST/Error.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ pgError authed e =
6464
else [jsonType] in
6565
responseLBS status hdrs (encodeError e)
6666

67-
singularityError :: Integer -> Response
67+
singularityError :: (Integral a, Show a) => a -> Response
6868
singularityError numRows =
6969
responseLBS HT.status406
7070
[toHeader CTSingularJSON]

test/Feature/InsertSpec.hs

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -385,18 +385,18 @@ spec actualPgVersion = do
385385
describe "Patching record" $ do
386386

387387
context "to unknown uri" $
388-
it "gives a 404" $
388+
it "indicates no table found by returning 404" $
389389
request methodPatch "/fake" []
390390
[json| { "real": false } |]
391391
`shouldRespondWith` 404
392392

393393
context "on an empty table" $
394-
it "indicates no records found to update" $
394+
it "indicates no records found to update by returning 404" $
395395
request methodPatch "/empty_table" []
396396
[json| { "extra":20 } |]
397397
`shouldRespondWith` ""
398-
{ matchStatus = 204,
399-
matchHeaders = ["Content-Range" <:> "*/*"]
398+
{ matchStatus = 404,
399+
matchHeaders = []
400400
}
401401

402402
context "in a nonempty table" $ do
@@ -423,10 +423,15 @@ spec actualPgVersion = do
423423
[("Prefer", "return=representation")] [json| { "id":999999 } |]
424424
`shouldRespondWith` "[]"
425425
{
426-
matchStatus = 200,
427-
matchHeaders = ["Content-Range" <:> "*/*"]
426+
matchStatus = 404,
427+
matchHeaders = []
428428
}
429429

430+
it "gives a 404 when no rows updated" $
431+
request methodPatch "/items?id=eq.99999999" []
432+
[json| { "id": 42 } |]
433+
`shouldRespondWith` 404
434+
430435
it "returns updated object as array when return=rep" $
431436
request methodPatch "/items?id=eq.2"
432437
[("Prefer", "return=representation")] [json| { "id":2 } |]
@@ -454,15 +459,26 @@ spec actualPgVersion = do
454459
[json| [{ a: "keepme", b: null }] |]
455460
{ matchHeaders = [matchContentTypeJson] }
456461

457-
it "can update based on a computed column" $
458-
request methodPatch
459-
"/items?always_true=eq.false"
460-
[("Prefer", "return=representation")]
461-
[json| { id: 100 } |]
462-
`shouldRespondWith` "[]"
463-
{ matchStatus = 200,
464-
matchHeaders = ["Content-Range" <:> "*/*"]
465-
}
462+
context "filtering by a computed column" $ do
463+
it "is successful" $
464+
request methodPatch
465+
"/items?is_first=eq.true"
466+
[("Prefer", "return=representation")]
467+
[json| { id: 100 } |]
468+
`shouldRespondWith` [json| [{ id: 100 }] |]
469+
{ matchStatus = 200,
470+
matchHeaders = [matchContentTypeJson, "Content-Range" <:> "0-0/*"]
471+
}
472+
473+
it "indicates no records updated by returning 404" $
474+
request methodPatch
475+
"/items?always_true=eq.false"
476+
[("Prefer", "return=representation")]
477+
[json| { id: 100 } |]
478+
`shouldRespondWith` "[]"
479+
{ matchStatus = 404,
480+
matchHeaders = []
481+
}
466482

467483
it "can provide a representation" $ do
468484
_ <- post "/items"

test/Feature/SingularSpec.hs

Lines changed: 52 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ spec =
4343
{ matchHeaders = ["Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"] }
4444

4545
context "when updating rows" $ do
46-
4746
it "works for one row" $ do
4847
_ <- post "/addresses" [json| { id: 97, address: "A Street" } |]
4948
request methodPatch
@@ -56,26 +55,26 @@ spec =
5655
it "raises an error for multiple rows" $ do
5756
_ <- post "/addresses" [json| { id: 98, address: "xxx" } |]
5857
_ <- post "/addresses" [json| { id: 99, address: "yyy" } |]
59-
p <- request methodPatch
60-
"/addresses?id=gt.0"
61-
[("Prefer", "return=representation"), singular]
62-
[json| { address: "zzz" } |]
58+
p <- request methodPatch "/addresses?id=gt.0"
59+
[("Prefer", "return=representation"), singular]
60+
[json| { address: "zzz" } |]
6361
liftIO $ do
6462
simpleStatus p `shouldBe` notAcceptable406
6563
isErrorFormat (simpleBody p) `shouldBe` True
6664

6765
-- the rows should not be updated, either
6866
get "/addresses?id=eq.98" `shouldRespondWith` [str|[{"id":98,"address":"xxx"}]|]
6967

70-
it "raises an error for zero rows" $ do
71-
p <- request methodPatch "/items?id=gt.0&id=lt.0"
72-
[("Prefer", "return=representation"), singular] [json|{"id":1}|]
73-
liftIO $ do
74-
simpleStatus p `shouldBe` notAcceptable406
75-
isErrorFormat (simpleBody p) `shouldBe` True
68+
it "raises an error for zero rows" $
69+
request methodPatch "/items?id=gt.0&id=lt.0"
70+
[("Prefer", "return=representation"), singular] [json|{"id":1}|]
71+
`shouldRespondWith`
72+
[str|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
73+
{ matchStatus = 406
74+
, matchHeaders = ["Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"]
75+
}
7676

7777
context "when creating rows" $ do
78-
7978
it "works for one row" $ do
8079
p <- request methodPost
8180
"/addresses"
@@ -117,17 +116,17 @@ spec =
117116
, matchHeaders = ["Content-Range" <:> "*/*"]
118117
}
119118

120-
it "raises an error when creating zero entities" $ do
121-
p <- request methodPost
122-
"/addresses"
123-
[("Prefer", "return=representation"), singular]
124-
[json| [ ] |]
125-
liftIO $ do
126-
simpleStatus p `shouldBe` notAcceptable406
127-
isErrorFormat (simpleBody p) `shouldBe` True
119+
it "raises an error when creating zero entities" $
120+
request methodPost "/addresses"
121+
[("Prefer", "return=representation"), singular]
122+
[json| [ ] |]
123+
`shouldRespondWith`
124+
[str|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
125+
{ matchStatus = 406
126+
, matchHeaders = ["Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"]
127+
}
128128

129129
context "when deleting rows" $ do
130-
131130
it "works for one row" $ do
132131
p <- request methodDelete
133132
"/items?id=eq.11"
@@ -146,21 +145,24 @@ spec =
146145
, matchHeaders = ["Content-Range" <:> "0-9/*"]
147146
}
148147

149-
it "raises an error when deleting zero entities" $ do
150-
p <- request methodDelete "/items?id=lt.0"
151-
[("Prefer", "return=representation"), singular] ""
152-
liftIO $ do
153-
simpleStatus p `shouldBe` notAcceptable406
154-
isErrorFormat (simpleBody p) `shouldBe` True
148+
it "raises an error when deleting zero entities" $
149+
request methodDelete "/items?id=lt.0"
150+
[("Prefer", "return=representation"), singular] ""
151+
`shouldRespondWith`
152+
[str|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
153+
{ matchStatus = 406
154+
, matchHeaders = ["Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"]
155+
}
155156

156157
context "when calling a stored proc" $ do
157-
158-
it "fails for zero rows" $ do
159-
p <- request methodPost "/rpc/getproject"
160-
[singular] [json|{ "id": 9999999}|]
161-
liftIO $ do
162-
simpleStatus p `shouldBe` notAcceptable406
163-
isErrorFormat (simpleBody p) `shouldBe` True
158+
it "fails for zero rows" $
159+
request methodPost "/rpc/getproject"
160+
[singular] [json|{ "id": 9999999}|]
161+
`shouldRespondWith`
162+
[str|{"details":"Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
163+
{ matchStatus = 406
164+
, matchHeaders = ["Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"]
165+
}
164166

165167
-- this one may be controversial, should vnd.pgrst.object include
166168
-- the likes of 2 and "hello?"
@@ -174,20 +176,26 @@ spec =
174176
[singular] [json|{ "id": 1}|] `shouldRespondWith`
175177
[str|{"id":1,"name":"Windows 7","client_id":1}|]
176178

177-
it "fails for multiple rows" $ do
178-
p <- request methodPost "/rpc/getallprojects" [singular] "{}"
179-
liftIO $ do
180-
simpleStatus p `shouldBe` notAcceptable406
181-
isErrorFormat (simpleBody p) `shouldBe` True
179+
it "fails for multiple rows" $
180+
request methodPost "/rpc/getallprojects"
181+
[singular] "{}"
182+
`shouldRespondWith`
183+
[str|{"details":"Results contain 5 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
184+
{ matchStatus = 406
185+
, matchHeaders = ["Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"]
186+
}
182187

183188
it "executes the proc exactly once per request" $ do
184189
request methodPost "/rpc/getproject?select=id,name" [] [json| {"id": 1} |]
185190
`shouldRespondWith` [str|[{"id":1,"name":"Windows 7"}]|]
186-
p <- request methodPost "/rpc/setprojects" [singular]
187-
[json| {"id_l": 1, "id_h": 2, "name": "changed"} |]
188-
liftIO $ do
189-
simpleStatus p `shouldBe` notAcceptable406
190-
isErrorFormat (simpleBody p) `shouldBe` True
191+
192+
request methodPost "/rpc/setprojects" [singular]
193+
[json| {"id_l": 1, "id_h": 2, "name": "changed"} |]
194+
`shouldRespondWith`
195+
[str|{"details":"Results contain 2 rows, application/vnd.pgrst.object+json requires 1 row","message":"JSON object requested, multiple (or no) rows returned"}|]
196+
{ matchStatus = 406
197+
, matchHeaders = ["Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"]
198+
}
191199

192200
-- should not actually have executed the function
193201
request methodPost "/rpc/getproject?select=id,name" [] [json| {"id": 1} |]

test/fixtures/schema.sql

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ CREATE FUNCTION always_true(test.items) RETURNS boolean
116116
LANGUAGE sql STABLE
117117
AS $$ SELECT true $$;
118118

119+
CREATE FUNCTION is_first(test.items) RETURNS boolean
120+
LANGUAGE sql STABLE
121+
AS $$ SELECT $1.id = 1 $$;
122+
123+
119124
CREATE FUNCTION anti_id(test.items) RETURNS bigint
120125
LANGUAGE sql STABLE
121126
AS $_$ SELECT $1.id * -1 $_$;

0 commit comments

Comments
 (0)