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:
committed by
Wolfgang Walther
parent
9ca9a54d21
commit
52d628f1ed
@@ -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
|
||||||
|
|||||||
@@ -469,6 +469,9 @@ handleInvoke invMethod proc context@RequestContext{..} = do
|
|||||||
RangeQuery.rangeStatusHeader iTopLevelRange queryTotal tableTotal
|
RangeQuery.rangeStatusHeader iTopLevelRange queryTotal tableTotal
|
||||||
|
|
||||||
failNotSingular iAcceptContentType queryTotal $
|
failNotSingular iAcceptContentType queryTotal $
|
||||||
|
if Proc.procReturnsVoid proc then
|
||||||
|
response HTTP.status204 [contentRange] mempty
|
||||||
|
else
|
||||||
response status
|
response status
|
||||||
(contentTypeHeaders context ++ [contentRange])
|
(contentTypeHeaders context ++ [contentRange])
|
||||||
(if invMethod == InvHead then mempty else LBS.fromStrict body)
|
(if invMethod == InvHead then mempty else LBS.fromStrict body)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
procReturnsVoid :: ProcDescription -> Bool
|
||||||
|
procReturnsVoid proc = case proc of
|
||||||
|
ProcDescription{pdReturnType = Nothing} -> True
|
||||||
_ -> False
|
_ -> 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
@@ -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)
|
||||||
|
|||||||
@@ -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")]
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user