fix: Return 204 No Content without Content-Type for RPCs returning VOID

Resolves #2001

BREAKING CHANGE: Previously, those RPCs would return "null" as a body with Content-Type: application/json.
This commit is contained in:
Wolfgang Walther
2022-02-05 09:28:28 +01:00
committed by Wolfgang Walther
parent 9ca9a54d21
commit 52d628f1ed
7 changed files with 66 additions and 33 deletions
+5
View File
@@ -33,6 +33,11 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #2135, Remove trigger functions from schema cache and OpenAPI output, because they can't be called directly anyway. - @wolfgangwalther - #2135, Remove trigger functions from schema cache and OpenAPI output, because they can't be called directly anyway. - @wolfgangwalther
- #2145, Fix accessing json array fields with -> and ->> in ?select= and ?order=. - @wolfgangwalther - #2145, Fix accessing json array fields with -> and ->> in ?select= and ?order=. - @wolfgangwalther
### Changed
- #2001, Return 204 No Content without Content-Type for RPCs returning VOID - @wolfgangwalther
+ Previously, those RPCs would return "null" as a body with Content-Type: application/json.
## [9.0.0] - 2021-11-25 ## [9.0.0] - 2021-11-25
### Added ### Added
+6 -3
View File
@@ -469,9 +469,12 @@ handleInvoke invMethod proc context@RequestContext{..} = do
RangeQuery.rangeStatusHeader iTopLevelRange queryTotal tableTotal RangeQuery.rangeStatusHeader iTopLevelRange queryTotal tableTotal
failNotSingular iAcceptContentType queryTotal $ failNotSingular iAcceptContentType queryTotal $
response status if Proc.procReturnsVoid proc then
(contentTypeHeaders context ++ [contentRange]) response HTTP.status204 [contentRange] mempty
(if invMethod == InvHead then mempty else LBS.fromStrict body) else
response status
(contentTypeHeaders context ++ [contentRange])
(if invMethod == InvHead then mempty else LBS.fromStrict body)
handleOpenApi :: Bool -> Schema -> RequestContext -> DbHandler Wai.Response handleOpenApi :: Bool -> Schema -> RequestContext -> DbHandler Wai.Response
handleOpenApi headersOnly tSchema (RequestContext conf@AppConfig{..} dbStructure apiRequest ctxPgVersion) = do handleOpenApi headersOnly tSchema (RequestContext conf@AppConfig{..} dbStructure apiRequest ctxPgVersion) = do
+7 -4
View File
@@ -202,6 +202,7 @@ decodeProcs =
<$> column HD.text <$> column HD.text
<*> column HD.text <*> column HD.text
<*> column HD.bool <*> column HD.bool
<*> column HD.bool
<*> column HD.bool) <*> column HD.bool)
<*> (parseVolatility <$> column HD.char) <*> (parseVolatility <$> column HD.char)
<*> column HD.bool <*> column HD.bool
@@ -209,10 +210,11 @@ decodeProcs =
addKey :: ProcDescription -> (QualifiedIdentifier, ProcDescription) addKey :: ProcDescription -> (QualifiedIdentifier, ProcDescription)
addKey pd = (QualifiedIdentifier (pdSchema pd) (pdName pd), pd) addKey pd = (QualifiedIdentifier (pdSchema pd) (pdName pd), pd)
parseRetType :: Text -> Text -> Bool -> Bool -> RetType parseRetType :: Text -> Text -> Bool -> Bool -> Bool -> Maybe RetType
parseRetType schema name isSetOf isComposite parseRetType schema name isSetOf isComposite isVoid
| isSetOf = SetOf pgType | isVoid = Nothing
| otherwise = Single pgType | isSetOf = Just (SetOf pgType)
| otherwise = Just (Single pgType)
where where
qi = QualifiedIdentifier schema name qi = QualifiedIdentifier schema name
pgType pgType
@@ -287,6 +289,7 @@ procsSqlQuery = [q|
-- if any TABLE, INOUT or OUT arguments present, treat as composite -- if any TABLE, INOUT or OUT arguments present, treat as composite
or COALESCE(proargmodes::text[] && '{t,b,o}', false) or COALESCE(proargmodes::text[] && '{t,b,o}', false)
) AS rettype_is_composite, ) AS rettype_is_composite,
('pg_catalog.void'::regtype = t.oid) AS rettype_is_void,
p.provolatile, p.provolatile,
p.provariadic > 0 as hasvariadic p.provariadic > 0 as hasvariadic
FROM pg_proc p FROM pg_proc p
+15 -9
View File
@@ -10,6 +10,7 @@ module PostgREST.DbStructure.Proc
, RetType(..) , RetType(..)
, procReturnsScalar , procReturnsScalar
, procReturnsSingle , procReturnsSingle
, procReturnsVoid
, procTableName , procTableName
) where ) where
@@ -42,7 +43,7 @@ data ProcDescription = ProcDescription
, pdName :: Text , pdName :: Text
, pdDescription :: Maybe Text , pdDescription :: Maybe Text
, pdParams :: [ProcParam] , pdParams :: [ProcParam]
, pdReturnType :: RetType , pdReturnType :: Maybe RetType
, pdVolatility :: ProcVolatility , pdVolatility :: ProcVolatility
, pdHasVariadic :: Bool , pdHasVariadic :: Bool
} }
@@ -69,17 +70,22 @@ type ProcsMap = M.HashMap QualifiedIdentifier [ProcDescription]
procReturnsScalar :: ProcDescription -> Bool procReturnsScalar :: ProcDescription -> Bool
procReturnsScalar proc = case proc of procReturnsScalar proc = case proc of
ProcDescription{pdReturnType = (Single Scalar)} -> True ProcDescription{pdReturnType = Just (Single Scalar)} -> True
ProcDescription{pdReturnType = (SetOf Scalar)} -> True ProcDescription{pdReturnType = Just (SetOf Scalar)} -> True
_ -> False _ -> False
procReturnsSingle :: ProcDescription -> Bool procReturnsSingle :: ProcDescription -> Bool
procReturnsSingle proc = case proc of procReturnsSingle proc = case proc of
ProcDescription{pdReturnType = (Single _)} -> True ProcDescription{pdReturnType = Just (Single _)} -> True
_ -> False _ -> False
procReturnsVoid :: ProcDescription -> Bool
procReturnsVoid proc = case proc of
ProcDescription{pdReturnType = Nothing} -> True
_ -> False
procTableName :: ProcDescription -> Maybe TableName procTableName :: ProcDescription -> Maybe TableName
procTableName proc = case pdReturnType proc of procTableName proc = case pdReturnType proc of
SetOf (Composite qi) -> Just $ qiName qi Just (SetOf (Composite qi)) -> Just $ qiName qi
Single (Composite qi) -> Just $ qiName qi Just (Single (Composite qi)) -> Just $ qiName qi
_ -> Nothing _ -> Nothing
+6 -6
View File
@@ -630,7 +630,7 @@ def test_jwt_secret_external_file_reload(tmp_path, defaultenv):
# reload config and external file with NOTIFY # reload config and external file with NOTIFY
response = postgrest.session.post("/rpc/reload_pgrst_config") response = postgrest.session.post("/rpc/reload_pgrst_config")
assert response.status_code == 200 assert response.status_code == 204
time.sleep(0.1) time.sleep(0.1)
response = postgrest.session.get("/authors_only", headers=headers) response = postgrest.session.get("/authors_only", headers=headers)
@@ -685,7 +685,7 @@ def test_db_schema_notify_reload(defaultenv):
# reset db-schemas config on the db # reset db-schemas config on the db
response = postgrest.session.post("/rpc/reset_db_schema_config") response = postgrest.session.post("/rpc/reset_db_schema_config")
assert response.status_code == 200 assert response.status_code == 204
def test_max_rows_reload(defaultenv): def test_max_rows_reload(defaultenv):
@@ -714,7 +714,7 @@ def test_max_rows_reload(defaultenv):
# reset max-rows config on the db # reset max-rows config on the db
response = postgrest.session.post("/rpc/reset_max_rows_config") response = postgrest.session.post("/rpc/reset_max_rows_config")
assert response.status_code == 200 assert response.status_code == 204
def test_max_rows_notify_reload(defaultenv): def test_max_rows_notify_reload(defaultenv):
@@ -744,7 +744,7 @@ def test_max_rows_notify_reload(defaultenv):
# reset max-rows config on the db # reset max-rows config on the db
response = postgrest.session.post("/rpc/reset_max_rows_config") response = postgrest.session.post("/rpc/reset_max_rows_config")
assert response.status_code == 200 assert response.status_code == 204
def test_invalid_role_claim_key_notify_reload(defaultenv): def test_invalid_role_claim_key_notify_reload(defaultenv):
@@ -770,7 +770,7 @@ def test_invalid_role_claim_key_notify_reload(defaultenv):
assert "failed to parse role-claim-key value" in output.decode() assert "failed to parse role-claim-key value" in output.decode()
response = postgrest.session.post("/rpc/reset_invalid_role_claim_key") response = postgrest.session.post("/rpc/reset_invalid_role_claim_key")
assert response.status_code == 200 assert response.status_code == 204
def test_db_prepared_statements_enable(defaultenv): def test_db_prepared_statements_enable(defaultenv):
@@ -835,7 +835,7 @@ def test_admin_ready_includes_schema_cache_state(defaultenv):
response = postgrest.session.post( response = postgrest.session.post(
"/rpc/no_schema_cache_for_limited_authenticator" "/rpc/no_schema_cache_for_limited_authenticator"
) )
assert response.status_code == 200 assert response.status_code == 204
# force a reconnection so the new role setting is picked up # force a reconnection so the new role setting is picked up
postgrest.process.send_signal(signal.SIGUSR1) postgrest.process.send_signal(signal.SIGUSR1)
+12 -3
View File
@@ -179,7 +179,10 @@ spec actualPgVersion = do
[("Prefer", "tx=commit")] [("Prefer", "tx=commit")]
[json|{"name": "auto_incrementing_pk_id_seq", "value": 2}|] [json|{"name": "auto_incrementing_pk_id_seq", "value": 2}|]
`shouldRespondWith` `shouldRespondWith`
[json|""|] ""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType ]
}
request methodPost "/auto_incrementing_pk" request methodPost "/auto_incrementing_pk"
[("Prefer", "return=headers-only")] [("Prefer", "return=headers-only")]
@@ -357,7 +360,10 @@ spec actualPgVersion = do
[("Prefer", "tx=commit")] [("Prefer", "tx=commit")]
[json|{"name": "items2_id_seq", "value": 20}|] [json|{"name": "items2_id_seq", "value": 20}|]
`shouldRespondWith` `shouldRespondWith`
[json|""|] ""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType ]
}
request methodPost "/items2" request methodPost "/items2"
[("Prefer", "return=representation")] [("Prefer", "return=representation")]
@@ -372,7 +378,10 @@ spec actualPgVersion = do
[("Prefer", "tx=commit")] [("Prefer", "tx=commit")]
[json|{"name": "items3_id_seq", "value": 20}|] [json|{"name": "items3_id_seq", "value": 20}|]
`shouldRespondWith` `shouldRespondWith`
[json|""|] ""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType ]
}
request methodPost "/items3?select=id" request methodPost "/items3?select=id"
[("Prefer", "return=representation")] [("Prefer", "return=representation")]
+15 -8
View File
@@ -329,18 +329,20 @@ spec actualPgVersion =
`shouldRespondWith` `shouldRespondWith`
[json|{"id": 2}|] [json|{"id": 2}|]
it "returns null for void" $ it "returns 204, no Content-Type header and no content for void" $
post "/rpc/ret_void" post "/rpc/ret_void"
[json|{}|] [json|{}|]
`shouldRespondWith` `shouldRespondWith`
[json|null|] ""
{ matchStatus = 204
, matchHeaders = [matchHeaderAbsent hContentType]
}
it "returns null for an integer with null value" $ it "returns null for an integer with null value" $
post "/rpc/ret_null" post "/rpc/ret_null"
[json|{}|] [json|{}|]
`shouldRespondWith` `shouldRespondWith`
"null" [json|null|]
{ matchHeaders = [matchContentTypeJson] }
context "different types when overloaded" $ do context "different types when overloaded" $ do
it "returns composite type" $ it "returns composite type" $
@@ -527,7 +529,10 @@ spec actualPgVersion =
[("Prefer", "tx=commit")] [("Prefer", "tx=commit")]
[json|{"name": "callcounter_count", "value": 1}|] [json|{"name": "callcounter_count", "value": 1}|]
`shouldRespondWith` `shouldRespondWith`
[json|""|] ""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType ]
}
-- now the test -- now the test
post "/rpc/callcounter" post "/rpc/callcounter"
@@ -1074,10 +1079,12 @@ spec actualPgVersion =
it "can set the same http header twice" $ it "can set the same http header twice" $
get "/rpc/set_cookie_twice" get "/rpc/set_cookie_twice"
`shouldRespondWith` `shouldRespondWith`
"null" ""
{ matchHeaders = [ matchContentTypeJson { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, "Set-Cookie" <:> "sessionid=38afes7a8; HttpOnly; Path=/" , "Set-Cookie" <:> "sessionid=38afes7a8; HttpOnly; Path=/"
, "Set-Cookie" <:> "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly" ]} , "Set-Cookie" <:> "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly" ]
}
it "can override the Location header on a trigger" $ it "can override the Location header on a trigger" $
post "/stuff" post "/stuff"