feat: add Content-Length response header

This commit is contained in:
Laurence Isla
2025-04-24 12:49:18 -05:00
committed by Steve Chavez
parent 98fcbedca5
commit 57ef9988a5
23 changed files with 298 additions and 114 deletions
+1
View File
@@ -21,6 +21,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
+ The selected columns in the embedded resources are aggregated into arrays
+ Aggregates are not supported
- #2967, Add `Proxy-Status` header for better error response - @taimoorzaeem
- #4016, Add `Content-Length` response header - @laurenceisla
### Fixed
+25 -4
View File
@@ -15,14 +15,14 @@ Observability allows measuring a system's current state based on the data it gen
Logs
====
PostgREST logs basic request information to ``stdout``, including the authenticated user if available, the requesting IP address and user agent, the URL requested, and HTTP response status.
PostgREST logs basic request information to ``stdout``, including the authenticated user if available, the requesting IP address and user agent, the URL requested, the HTTP response status and the response body size in bytes if available.
With :ref:`log-level` set to ``info``, we get:
.. code::
127.0.0.1 - user [26/Jul/2021:01:56:38 -0500] "GET /clients HTTP/1.1" 200 - "" "curl/7.64.0"
127.0.0.1 - anonymous [26/Jul/2021:01:56:48 -0500] "GET /unexistent HTTP/1.1" 404 - "" "curl/7.64.0"
127.0.0.1 - user [26/Jul/2021:01:56:38 -0500] "GET /clients HTTP/1.1" 200 56 "" "curl/7.64.0"
127.0.0.1 - anonymous [26/Jul/2021:01:56:48 -0500] "GET /unexistent HTTP/1.1" 404 162 "" "curl/7.64.0"
For diagnostic information about the server itself, PostgREST logs to ``stderr``:
@@ -73,7 +73,7 @@ This will be logged by PostgREST:
.. code::
17/Feb/2025:17:28:15 -0500: WITH pgrst_source AS ( SELECT "public"."protected_table".* FROM "public"."protected_table" ) SELECT null::bigint AS total_result_set, pg_catalog.count(_postgrest_t) AS page_total, coalesce(json_agg(_postgrest_t), '[]') AS body, nullif(current_setting('response.headers', true), '') AS response_headers, nullif(current_setting('response.status', true), '') AS response_status, '' AS response_inserted FROM ( SELECT * FROM pgrst_source ) _postgrest_t
127.0.0.1 - web_anon [17/Feb/2025:17:28:15 -0500] "GET /protected_table HTTP/1.1" 401 - "" "curl/8.7.1"
127.0.0.1 - web_anon [17/Feb/2025:17:28:15 -0500] "GET /protected_table HTTP/1.1" 401 99 "" "curl/8.7.1"
Database Logs
-------------
@@ -270,6 +270,27 @@ This header communicates metrics of the different phases in the request-response
We're working on lowering the duration of the ``parse`` and ``plan`` stages on https://github.com/PostgREST/postgrest/issues/2816.
.. _content-length_header:
Content-Length Header
---------------------
You can verify the response body size in bytes in the `Content-Length header <https://httpwg.org/specs/rfc9110.html#field.content-length>`_.
.. code-block:: bash
curl -i 'localhost:3000/users'
.. code-block:: http
HTTP/1.1 200 OK
Content-Length: 104
Note that this header won't be returned on ``HEAD`` requests for optimization purposes (see :ref:`head_req`).
This is in line with `RFC 9110 <https://httpwg.org/specs/rfc9110.html#field.content-length>`_.
The body size is also present in the :ref:`PostgREST logs <pgrst_logging>`.
.. _explain_plan:
Execution plan
+6 -2
View File
@@ -20,6 +20,7 @@ module PostgREST.Error
import qualified Data.Aeson as JSON
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as LBS
import qualified Data.CaseInsensitive as CI
import qualified Data.FuzzySet as Fuzzy
import qualified Data.HashMap.Strict as HM
@@ -59,8 +60,11 @@ class (ErrorBody a, JSON.ToJSON a) => PgrstError a where
errorResponseFor :: a -> Response
errorResponseFor err =
let baseHeader = MediaType.toContentType MTApplicationJSON in
responseLBS (status err) (baseHeader : headers err) $ errorPayload err
let
baseHeader = MediaType.toContentType MTApplicationJSON
cLHeader body = (,) "Content-Length" (show $ LBS.length body) :: Header
in
responseLBS (status err) (baseHeader : cLHeader (errorPayload err) : headers err) $ errorPayload err
class ErrorBody a where
code :: a -> Text
+32 -15
View File
@@ -68,6 +68,7 @@ actionResponse (DbCrudResult WrappedReadPlan{wrMedia, wrHdrsOnly=headersOnly, cr
RSStandard{..} -> do
let
(status, contentRange) = RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal
cLHeader = if headersOnly then mempty else [contentLengthHeaderStrict rsBody]
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing preferCount preferTransaction Nothing preferHandling preferTimezone Nothing []
headers =
[ contentRange
@@ -77,6 +78,7 @@ actionResponse (DbCrudResult WrappedReadPlan{wrMedia, wrHdrsOnly=headersOnly, cr
<> if BS.null (qsCanonical iQueryParams) then mempty else "?" <> qsCanonical iQueryParams
)
]
++ cLHeader
++ contentTypeHeaders wrMedia ctxApiRequest
++ prefHeader
@@ -90,7 +92,7 @@ actionResponse (DbCrudResult WrappedReadPlan{wrMedia, wrHdrsOnly=headersOnly, cr
Right $ PgrstResponse ovStatus ovHeaders bod
RSPlan plan ->
Right $ PgrstResponse HTTP.status200 (contentTypeHeaders wrMedia ctxApiRequest) $ LBS.fromStrict plan
Right $ PgrstResponse HTTP.status200 (contentLengthHeaderStrict plan : contentTypeHeaders wrMedia ctxApiRequest) $ LBS.fromStrict plan
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationCreate, mrMutatePlan, mrMedia, crudQi=QualifiedIdentifier{..}} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}, ..} _ _ _ _ _ = case resultSet of
RSStandard{..} -> do
@@ -112,6 +114,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationCreate, mrMutateP
)
, Just . RangeQuery.contentRangeH 1 0 $
if shouldCount preferCount then Just rsQueryTotal else Nothing
, Just $ contentLengthHeaderStrict rsBody
, prefHeader ]
let isInsertIfGTZero i =
@@ -130,7 +133,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationCreate, mrMutateP
Right $ PgrstResponse ovStatus ovHeaders bod
RSPlan plan ->
Right $ PgrstResponse HTTP.status200 (contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
Right $ PgrstResponse HTTP.status200 (contentLengthHeaderStrict plan : contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationUpdate, mrMedia} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = case resultSet of
RSStandard{..} -> do
@@ -143,7 +146,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationUpdate, mrMedia}
let (status, headers', body) =
case preferRepresentation of
Just Full -> (HTTP.status200, headers ++ contentTypeHeaders mrMedia ctxApiRequest, LBS.fromStrict rsBody)
Just Full -> (HTTP.status200, headers ++ [contentLengthHeaderStrict rsBody] ++ contentTypeHeaders mrMedia ctxApiRequest, LBS.fromStrict rsBody)
Just None -> (HTTP.status204, headers, mempty)
_ -> (HTTP.status204, headers, mempty)
@@ -152,19 +155,20 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationUpdate, mrMedia}
Right $ PgrstResponse ovStatus ovHeaders body
RSPlan plan ->
Right $ PgrstResponse HTTP.status200 (contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
Right $ PgrstResponse HTTP.status200 (contentLengthHeaderStrict plan : contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationSingleUpsert, mrMedia} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = case resultSet of
RSStandard {..} -> do
let
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation preferCount preferTransaction Nothing preferHandling preferTimezone Nothing []
cLHeader = [contentLengthHeaderStrict rsBody]
cTHeader = contentTypeHeaders mrMedia 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, cTHeader ++ prefHeader, LBS.fromStrict rsBody)
Just Full -> (upsertStatus, cLHeader ++ cTHeader ++ prefHeader, LBS.fromStrict rsBody)
Just None -> (HTTP.status204, prefHeader, mempty)
_ -> (HTTP.status204, prefHeader, mempty)
(ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status headers
@@ -172,7 +176,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationSingleUpsert, mrM
Right $ PgrstResponse ovStatus ovHeaders body
RSPlan plan ->
Right $ PgrstResponse HTTP.status200 (contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
Right $ PgrstResponse HTTP.status200 (contentLengthHeaderStrict plan : contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationDelete, mrMedia} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = case resultSet of
RSStandard {..} -> do
@@ -185,7 +189,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationDelete, mrMedia}
let (status, headers', body) =
case preferRepresentation of
Just Full -> (HTTP.status200, headers ++ contentTypeHeaders mrMedia ctxApiRequest, LBS.fromStrict rsBody)
Just Full -> (HTTP.status200, headers ++ [contentLengthHeaderStrict rsBody] ++ contentTypeHeaders mrMedia ctxApiRequest, LBS.fromStrict rsBody)
Just None -> (HTTP.status204, headers, mempty)
_ -> (HTTP.status204, headers, mempty)
@@ -194,7 +198,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationDelete, mrMedia}
Right $ PgrstResponse ovStatus ovHeaders body
RSPlan plan ->
Right $ PgrstResponse HTTP.status200 (contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
Right $ PgrstResponse HTTP.status200 (contentLengthHeaderStrict plan : contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
actionResponse (DbCallResult CallReadPlan{crMedia, crInvMthd=invMethod, crProc=proc} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..},..} _ _ _ _ _ = case resultSet of
RSStandard {..} -> do
@@ -205,7 +209,9 @@ actionResponse (DbCallResult CallReadPlan{crMedia, crInvMthd=invMethod, crProc=p
then Error.errorPayload $ Error.ApiRequestError $ Error.InvalidRange
$ Error.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal)
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]
headers = contentRange : prefHeader
let (status', headers', body) =
@@ -213,20 +219,22 @@ actionResponse (DbCallResult CallReadPlan{crMedia, crInvMthd=invMethod, crProc=p
(HTTP.status204, headers, mempty)
else
(status,
headers ++ contentTypeHeaders crMedia ctxApiRequest,
if invMethod == InvRead True then mempty else rsOrErrBody)
headers ++ cLHeader ++ contentTypeHeaders crMedia ctxApiRequest,
if isHeadMethod then mempty else rsOrErrBody)
(ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status' headers'
Right $ PgrstResponse ovStatus ovHeaders body
RSPlan plan ->
Right $ PgrstResponse HTTP.status200 (contentTypeHeaders crMedia ctxApiRequest) $ LBS.fromStrict plan
Right $ PgrstResponse HTTP.status200 (contentLengthHeaderStrict plan : contentTypeHeaders crMedia ctxApiRequest) $ LBS.fromStrict plan
actionResponse (MaybeDbResult InspectPlan{ipHdrsOnly=headersOnly} body) _ versions conf sCache schema negotiatedByProfile =
Right $ PgrstResponse HTTP.status200
(MediaType.toContentType MTOpenAPI : maybeToList (profileHeader schema negotiatedByProfile))
(maybe mempty (\(x, y, z) -> if headersOnly then mempty else OpenAPI.encode versions conf sCache x y z) body)
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]
in
Right $ PgrstResponse HTTP.status200 (MediaType.toContentType MTOpenAPI : cLHeader ++ maybeToList (profileHeader schema negotiatedByProfile)) rsBody
actionResponse (NoDbResult (RelInfoPlan qi@QualifiedIdentifier{..})) _ _ _ SchemaCache{dbTables} _ _ =
case HM.lookup qi dbTables of
@@ -251,7 +259,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 [allOrigins, (HTTP.hAllow, allowHeader)] mempty
Right $ PgrstResponse HTTP.status200 [contentLengthHeaderStrict 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])
@@ -268,6 +276,15 @@ 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
contentTypeHeaders :: MediaType -> ApiRequest -> [HTTP.Header]
contentTypeHeaders mediaType ApiRequest{..} =
MediaType.toContentType mediaType : maybeToList (profileHeader iSchema iNegotiatedByProfile)
+9 -9
View File
@@ -964,45 +964,45 @@ def test_log_level(level, defaultenv):
assert len(output) == 0
elif level == "error":
assert re.match(
r'- - - \[.+\] "GET / HTTP/1.1" 500 - "" "python-requests/.+"',
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
output[0],
)
assert len(output) == 1
elif level == "warn":
assert re.match(
r'- - - \[.+\] "GET / HTTP/1.1" 500 - "" "python-requests/.+"',
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
output[0],
)
assert re.match(
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 - "" "python-requests/.+"',
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"',
output[1],
)
assert len(output) == 2
elif level == "info":
assert re.match(
r'- - - \[.+\] "GET / HTTP/1.1" 500 - "" "python-requests/.+"',
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
output[0],
)
assert re.match(
r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 - "" "python-requests/.+"',
r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 \d+ "" "python-requests/.+"',
output[1],
)
assert re.match(
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 - "" "python-requests/.+"',
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"',
output[2],
)
assert len(output) == 3
elif level == "debug":
assert re.match(
r'- - - \[.+\] "GET / HTTP/1.1" 500 - "" "python-requests/.+"',
r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
output[0],
)
assert re.match(
r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 - "" "python-requests/.+"',
r'- - postgrest_test_anonymous \[.+\] "GET / HTTP/1.1" 200 \d+ "" "python-requests/.+"',
output[1],
)
assert re.match(
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 - "" "python-requests/.+"',
r'- - postgrest_test_anonymous \[.+\] "GET /unknown HTTP/1.1" 404 \d+ "" "python-requests/.+"',
output[2],
)
+5 -3
View File
@@ -22,7 +22,8 @@ spec = describe "authorization" $ do
"code":"42501",
"message":"permission denied for table authors_only"} |]
{ matchStatus = 401
, matchHeaders = ["WWW-Authenticate" <:> "Bearer"]
, matchHeaders = [ "WWW-Authenticate" <:> "Bearer"
, "Content-Length" <:> "96" ]
}
it "denies access to tables that postgrest_test_author does not own" $
@@ -35,7 +36,7 @@ spec = describe "authorization" $ do
"code":"42501",
"message":"permission denied for table private_table"} |]
{ matchStatus = 403
, matchHeaders = []
, matchHeaders = ["Content-Length" <:> "97"]
}
it "denies execution on functions that anonymous does not own" $
@@ -92,7 +93,8 @@ spec = describe "authorization" $ do
{ matchStatus = 401
, matchHeaders = [
"WWW-Authenticate" <:>
"Bearer error=\"invalid_token\", error_description=\"Empty JWT is sent in Authorization header\""
"Bearer error=\"invalid_token\", error_description=\"Empty JWT is sent in Authorization header\"",
"Content-Length" <:> "100"
]
}
@@ -17,4 +17,5 @@ spec =
[("Accept","application/openapi+json")] ""
`shouldRespondWith`
[json| {"code":"PGRST126","details":null,"hint":null,"message":"Root endpoint metadata is disabled"} |]
{ matchStatus = 404 }
{ matchStatus = 404
, matchHeaders = ["Content-Length" <:> "93"]}
@@ -25,7 +25,8 @@ spec = describe "OpenAPI Ignore Privileges" $ do
`shouldRespondWith`
""
{ matchStatus = 200
, matchHeaders = [ "Content-Type" <:> "application/openapi+json; charset=utf-8" ]
, matchHeaders = [ "Content-Type" <:> "application/openapi+json; charset=utf-8"
, matchHeaderAbsent hContentLength]
}
describe "table" $ do
+8 -4
View File
@@ -24,7 +24,8 @@ spec = describe "OpenAPI" $ do
`shouldRespondWith`
""
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/openapi+json; charset=utf-8"]
, matchHeaders = [ "Content-Type" <:> "application/openapi+json; charset=utf-8"
, matchHeaderAbsent hContentLength ]
}
it "should respond to openapi request on none root path with 406" $
@@ -38,11 +39,14 @@ spec = describe "OpenAPI" $ do
`shouldRespondWith` 406
it "includes postgrest.org current version api docs" $ do
r <- simpleBody <$> get "/"
r <- get "/"
let docsUrl = r ^? key "externalDocs" . key "url"
let headers = simpleHeaders r
docsUrl = simpleBody r ^? key "externalDocs" . key "url"
liftIO $ docsUrl `shouldBe` Just (String ("https://postgrest.org/en/" <> docsVersion <> "/references/api.html"))
liftIO $ do
headers `shouldSatisfy` notZeroContentLength
docsUrl `shouldBe` Just (String ("https://postgrest.org/en/" <> docsVersion <> "/references/api.html"))
describe "schema" $ do
+3 -3
View File
@@ -15,9 +15,9 @@ spec = describe "Allow header" $ do
context "a table" $ do
it "includes read/write methods for writeable table" $ do
r <- request methodOptions "/items" [] ""
liftIO $
simpleHeaders r `shouldSatisfy`
matchHeader "Allow" "OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE"
liftIO $ do
simpleHeaders r `shouldSatisfy` matchHeader "Allow" "OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE"
simpleHeaders r `shouldSatisfy` matchHeader "Content-Length" "0"
it "fails with 404 for an unknown table" $
request methodOptions "/unknown" [] "" `shouldRespondWith` 404
+29 -12
View File
@@ -20,6 +20,7 @@ spec = describe "custom media types" $ do
liftIO $ do
simpleBody r `shouldBe` readFixtureFile "lines.twkb"
simpleHeaders r `shouldContain` [("Content-Type", "application/vnd.twkb")]
simpleHeaders r `shouldContain` [("Content-Length", "30")]
it "can query by id if there's an aggregate defined for the table" $ do
r <- request methodGet "/lines?id=eq.1" (acceptHdrs "application/vnd.twkb") ""
@@ -32,7 +33,8 @@ spec = describe "custom media types" $ do
`shouldRespondWith`
[json| {"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: text/plain"} |]
{ matchStatus = 406
, matchHeaders = [matchContentTypeJson]
, matchHeaders = [ matchContentTypeJson
, "Content-Length" <:> "110" ]
}
it "can get raw xml output with Accept: text/xml if there's an aggregate defined" $ do
@@ -40,7 +42,8 @@ spec = describe "custom media types" $ do
`shouldRespondWith`
"<myxml>foo</myxml>bar<foobar><baz/></foobar>"
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "text/xml; charset=utf-8"]
, matchHeaders = [ "Content-Type" <:> "text/xml; charset=utf-8"
, "Content-Length" <:> "44"]
}
-- TODO SOH (start of heading) is being added to results
@@ -50,7 +53,8 @@ spec = describe "custom media types" $ do
`shouldRespondWith`
"\SOH{\"type\": \"FeatureCollection\", \"hello\": \"world\"}"
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/vnd.geo2+json"]
, matchHeaders = [ "Content-Type" <:> "application/vnd.geo2+json"
, "Content-Length" <:> "48" ]
}
it "will use the more specific application/vnd.geo2 handler for this table" $ do
@@ -83,14 +87,16 @@ spec = describe "custom media types" $ do
|</html>
|]
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "text/html"]
, matchHeaders = [ "Content-Type" <:> "text/html"
, "Content-Length" <:> "117" ]
}
it "can get raw output with Accept: text/plain" $ do
request methodGet "/rpc/welcome" (acceptHdrs "text/plain") ""
`shouldRespondWith` "Welcome to PostgREST"
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "text/plain; charset=utf-8"]
, matchHeaders = [ "Content-Type" <:> "text/plain; charset=utf-8"
, "Content-Length" <:> "20" ]
}
it "can get raw xml output with Accept: text/xml" $ do
@@ -98,7 +104,8 @@ spec = describe "custom media types" $ do
`shouldRespondWith`
"<my-xml-tag/>"
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "text/xml; charset=utf-8"]
, matchHeaders = [ "Content-Type" <:> "text/xml; charset=utf-8"
, "Content-Length" <:> "13" ]
}
it "can get raw xml output with Accept: text/xml" $ do
@@ -132,6 +139,7 @@ spec = describe "custom media types" $ do
liftIO $ do
simpleBody r `shouldBe` readFixtureFile "A.png"
simpleHeaders r `shouldContain` [("Content-Type", "image/png")]
simpleHeaders r `shouldContain` [("Content-Length", "138")]
context "Proc that returns set of scalars and Accept: text/plain" $
it "will err because only scalars work with media type domains" $ do
@@ -141,7 +149,8 @@ spec = describe "custom media types" $ do
`shouldRespondWith`
[json|{"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: text/plain"}|]
{ matchStatus = 406
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
, matchHeaders = [ "Content-Type" <:> "application/json; charset=utf-8"
, "Content-Length" <:> "110"]
}
context "Proc that returns rows and accepts custom media type" $ do
@@ -173,7 +182,8 @@ spec = describe "custom media types" $ do
`shouldRespondWith`
[json| {"overridden": "true"} |]
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
, matchHeaders = [ "Content-Type" <:> "application/json; charset=utf-8"
, "Content-Length" <:> "22" ]
}
-- TODO SOH (start of heading) is being added to results
@@ -182,7 +192,8 @@ spec = describe "custom media types" $ do
`shouldRespondWith`
"\SOH{\"crs\": {\"type\": \"name\", \"properties\": {\"name\": \"EPSG:4326\"}}, \"type\": \"FeatureCollection\", \"features\": [{\"type\": \"Feature\", \"geometry\": {\"type\": \"LineString\", \"coordinates\": [[1, 1], [5, 5]]}, \"properties\": {\"id\": 1, \"name\": \"line-1\"}}]}"
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/geo+json; charset=utf-8"]
, matchHeaders = [ "Content-Type" <:> "application/geo+json; charset=utf-8"
, "Content-Length" <:> "239" ]
}
it "will not override vendored media types like application/vnd.pgrst.object" $
@@ -190,7 +201,8 @@ spec = describe "custom media types" $ do
`shouldRespondWith`
[json|{"id":1,"name":"Windows 7","client_id":1}|]
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"]
, matchHeaders = [ "Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"
, "Content-Length" <:> "41" ]
}
context "matches requested media type correctly" $ do
@@ -200,11 +212,13 @@ spec = describe "custom media types" $ do
liftIO $ do
simpleBody r1 `shouldBe` readFixtureFile "A.png"
simpleHeaders r1 `shouldContain` [("Content-Type", "image/png")]
simpleHeaders r1 `shouldContain` [("Content-Length", "138")]
r2 <- request methodGet "/rpc/ret_image" (acceptHdrs "text/html,application/xhtml+xml,application/xml;q=0.9,image/png,*/*;q=0.8") ""
liftIO $ do
simpleBody r2 `shouldBe` readFixtureFile "A.png"
simpleHeaders r2 `shouldContain` [("Content-Type", "image/png")]
simpleHeaders r2 `shouldContain` [("Content-Length", "138")]
-- https://github.com/PostgREST/postgrest/issues/2170
it "will match json in presence of text/plain" $ do
@@ -219,7 +233,8 @@ spec = describe "custom media types" $ do
`shouldRespondWith`
"id\tname\tclient_id\n1\tWindows 7\t1\n2\tWindows 10\t1\n"
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "text/tab-separated-values"]
, matchHeaders = [ "Content-Type" <:> "text/tab-separated-values"
, "Content-Length" <:> "47" ]
}
-- https://github.com/PostgREST/postgrest/issues/1371#issuecomment-519248984
@@ -229,6 +244,7 @@ spec = describe "custom media types" $ do
simpleBody r `shouldBe` readFixtureFile "lines.csv"
simpleHeaders r `shouldContain` [("Content-Type", "text/csv; charset=utf-8")]
simpleHeaders r `shouldContain` [("Content-Disposition", "attachment; filename=\"lines.csv\"")]
simpleHeaders r `shouldContain` [("Content-Length", "216")]
-- https://github.com/PostgREST/postgrest/issues/3160
context "using select query parameter" $ do
@@ -239,7 +255,8 @@ spec = describe "custom media types" $ do
|(2,"Windows 10",1)
|]
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "pg/outfunc"]
, matchHeaders = [ "Content-Type" <:> "pg/outfunc"
, "Content-Length" <:> "37" ]
}
it "with fewer columns selected" $ do
+9 -2
View File
@@ -22,6 +22,7 @@ spec =
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "*/*" ]
}
@@ -30,6 +31,7 @@ spec =
`shouldRespondWith` [json|[{"id":2}]|]
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/1"
, "Content-Length" <:> "10"
, "Preference-Applied" <:> "return=representation, count=exact"]
}
@@ -41,6 +43,7 @@ spec =
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "*/*" ]
}
request methodDelete "/items?id=eq.3&select=id"
@@ -50,6 +53,7 @@ spec =
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "*/*"
, "Preference-Applied" <:> "return=minimal"]
}
@@ -58,7 +62,8 @@ spec =
request methodDelete "/complex_items?id=eq.2&select=id,name" [("Prefer", "return=representation")] ""
`shouldRespondWith` [json|[{"id":2,"name":"Two"}]|]
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/*"]
, matchHeaders = [ "Content-Range" <:> "*/*"
, "Content-Length" <:> "23" ]
}
it "can rename and cast the selected columns" $
@@ -137,6 +142,7 @@ spec =
""
{ matchStatus = 204
, matchHeaders = [matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Preference-Applied" <:> "return=minimal" ]
}
@@ -147,7 +153,8 @@ spec =
`shouldRespondWith`
""
{ matchStatus = 204
, matchHeaders = [matchHeaderAbsent hContentType]
, matchHeaders = [matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength]
}
context "with ordering" $
@@ -40,7 +40,8 @@ spec =
}
|]
{ matchStatus = 300
, matchHeaders = [matchContentTypeJson]
, matchHeaders = [ matchContentTypeJson
, "Content-Length" <:> "828" ]
}
it "errs on an ambiguous embed that has a circular reference" $
+12 -6
View File
@@ -26,7 +26,8 @@ pgErrorCodeMapping = do
"details": null,
"hint": "Increase the configuration parameter \"max_stack_depth\" (currently 2048kB), after ensuring the platform's stack depth limit is adequate.",
"message": "stack depth limit exceeded"}|]
{ matchStatus = 500 }
{ matchStatus = 500
, matchHeaders = ["Content-Length" <:> "217"] }
context "includes the proxy-status header on the response" $ do
it "works with ApiRequest error" $
@@ -34,7 +35,8 @@ pgErrorCodeMapping = do
`shouldRespondWith`
[json| {"code":"PGRST125","details":null,"hint":null,"message":"Invalid path specified in request URL"} |]
{ matchStatus = 404
, matchHeaders = ["Proxy-Status" <:> "PostgREST; error=PGRST125"]
, matchHeaders = [ "Proxy-Status" <:> "PostgREST; error=PGRST125"
, "Content-Length" <:> "96" ]
}
it "works with SchemaCache error" $
@@ -42,7 +44,8 @@ pgErrorCodeMapping = do
`shouldRespondWith`
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.json_table'","message":"Could not find the table 'test.non_existent_table' in the schema cache"} |]
{ matchStatus = 404
, matchHeaders = ["Proxy-Status" <:> "PostgREST; error=PGRST205"]
, matchHeaders = [ "Proxy-Status" <:> "PostgREST; error=PGRST205"
, "Content-Length" <:> "172" ]
}
it "works with Jwt error" $ do
@@ -51,7 +54,8 @@ pgErrorCodeMapping = do
`shouldRespondWith`
[json| {"message":"Expected 3 parts in JWT; got 2","code":"PGRST301","hint":null,"details":null} |]
{ matchStatus = 401
, matchHeaders = ["Proxy-Status" <:> "PostgREST; error=PGRST301"]
, matchHeaders = [ "Proxy-Status" <:> "PostgREST; error=PGRST301"
, "Content-Length" <:> "89" ]
}
it "works with raise sqlstate custom error" $
@@ -59,7 +63,8 @@ pgErrorCodeMapping = do
`shouldRespondWith`
[json| {"code":"PT402","details":"Quota exceeded","hint":"Upgrade your plan","message":"Payment Required"} |]
{ matchStatus = 402
, matchHeaders = ["Proxy-Status" <:> "PostgREST; error=PT402"]
, matchHeaders = [ "Proxy-Status" <:> "PostgREST; error=PT402"
, "Content-Length" <:> "99" ]
}
it "works with sqlstate PGRST custom error" $
@@ -67,5 +72,6 @@ pgErrorCodeMapping = do
`shouldRespondWith`
[json| {"code":"123","details":"DEF","hint":"XYZ","message":"ABC"} |]
{ matchStatus = 332
, matchHeaders = ["Proxy-Status" <:> "PostgREST; error=123"]
, matchHeaders = [ "Proxy-Status" <:> "PostgREST; error=123"
, "Content-Length" <:> "59" ]
}
+14 -7
View File
@@ -30,7 +30,8 @@ spec actualPgVersion = do
} |] `shouldRespondWith` ""
{ matchStatus = 201
-- should not have content type set when body is empty
, matchHeaders = [matchHeaderAbsent hContentType]
, matchHeaders = [ matchHeaderAbsent hContentType
, "Content-Length" <:> "0"]
}
it "filters columns in result using &select" $
@@ -42,6 +43,7 @@ spec actualPgVersion = do
}] |] `shouldRespondWith` [json|[{"integer":14,"varchar":"testing!"}]|]
{ matchStatus = 201
, matchHeaders = [matchContentTypeJson
, "Content-Length" <:> "37"
, "Preference-Applied" <:> "return=representation"]
}
@@ -187,7 +189,8 @@ spec actualPgVersion = do
`shouldRespondWith`
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType ]
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength ]
}
request methodPost "/auto_incrementing_pk"
@@ -211,7 +214,7 @@ spec actualPgVersion = do
[json|{"hint":null,"details":"Failing row contains (null, foo).","code":"23502","message":"null value in column \"k\" violates not-null constraint"}|]
)
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
, matchHeaders = [ matchContentTypeJson]
}
context "into a table with no pk" $ do
@@ -286,7 +289,8 @@ spec actualPgVersion = do
`shouldRespondWith`
[json|{"message":"Empty or invalid json","code":"PGRST102","details":null,"hint":null}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
, matchHeaders = [ matchContentTypeJson,
"Content-Length" <:> "80" ]
}
context "with no payload" $
@@ -370,7 +374,8 @@ spec actualPgVersion = do
`shouldRespondWith`
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType ]
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength ]
}
request methodPost "/items2"
@@ -388,7 +393,8 @@ spec actualPgVersion = do
`shouldRespondWith`
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType ]
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength ]
}
request methodPost "/items3?select=id"
@@ -623,7 +629,8 @@ spec actualPgVersion = do
`shouldRespondWith`
""
{ matchStatus = 204
, matchHeaders = [matchHeaderAbsent hContentType]
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength ]
}
describe "CSV insert" $ do
+10
View File
@@ -33,6 +33,7 @@ spec actualPgVersion = do
liftIO $ do
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
resHeaders `shouldSatisfy` notZeroContentLength
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
totalCost `shouldBe` (if actualPgVersion >= pgVersion170 then 11.32 else 15.63)
@@ -140,6 +141,7 @@ spec actualPgVersion = do
liftIO $ do
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
resHeaders `shouldSatisfy` notZeroContentLength
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
totalCost `shouldBe` 0.06
@@ -153,6 +155,7 @@ spec actualPgVersion = do
liftIO $ do
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
resHeaders `shouldSatisfy` notZeroContentLength
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
totalCost `shouldBe` 8.23
@@ -166,6 +169,7 @@ spec actualPgVersion = do
liftIO $ do
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
resHeaders `shouldSatisfy` notZeroContentLength
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
totalCost `shouldBe` (if actualPgVersion >= pgVersion170 then 11.37 else 15.68)
@@ -180,6 +184,7 @@ spec actualPgVersion = do
liftIO $ do
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
resHeaders `shouldSatisfy` notZeroContentLength
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
totalCost `shouldBe` 3.55
@@ -194,6 +199,7 @@ spec actualPgVersion = do
liftIO $ do
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
resHeaders `shouldSatisfy` notZeroContentLength
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
totalCost `shouldBe` 5.53
@@ -248,6 +254,7 @@ spec actualPgVersion = do
liftIO $ do
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/vnd.pgrst.object+json\"; options=verbose; charset=utf-8")
resHeaders `shouldSatisfy` notZeroContentLength
aggCol `shouldBe` Just [aesonQQ| "COALESCE((json_agg(ROW(projects.id, projects.name, projects.client_id)) -> 0), 'null'::json)" |]
describe "function plan" $ do
@@ -261,6 +268,7 @@ spec actualPgVersion = do
liftIO $ do
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8")
resHeaders `shouldSatisfy` notZeroContentLength
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
totalCost `shouldBe` 68.56
@@ -275,6 +283,7 @@ spec actualPgVersion = do
liftIO $ do
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+text; for=\"application/json\"; charset=utf-8")
resHeaders `shouldSatisfy` notZeroContentLength
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
resBody `shouldSatisfy` (\t -> LBS.take 9 t == "Aggregate")
@@ -288,6 +297,7 @@ spec actualPgVersion = do
liftIO $ do
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+text; for=\"application/json\"; charset=utf-8")
resHeaders `shouldSatisfy` notZeroContentLength
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
resBody `shouldSatisfy` (\t -> LBS.take 9 t == "Aggregate")
+45 -15
View File
@@ -28,14 +28,17 @@ spec = do
`shouldRespondWith`
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.private_table'","message":"Could not find the table 'test.faketable' in the schema cache"} |]
{ matchStatus = 404
, matchHeaders = []
, matchHeaders = ["Content-Length" <:> "166"]
}
describe "Filtering response" $ do
it "matches with equality" $
get "/items?id=eq.5"
`shouldRespondWith` [json| [{"id":5}] |]
{ matchHeaders = ["Content-Range" <:> "0-0/*"] }
{ matchHeaders = [
"Content-Range" <:> "0-0/*",
"Content-Length" <:> "10"
] }
it "matches with equality using not operator" $
get "/items?id=not.eq.5&order=id"
@@ -511,7 +514,10 @@ spec = do
"code":"PGRST108",
"message":"'non_existent_projects' is not an embedded resource in this request"}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
, matchHeaders = [
"Content-Length" <:> "204",
matchContentTypeJson
]
}
get "/clients?select=*,amiga_projects:projects(*)&amiga_projectsss.name=ilike.*Amiga*" `shouldRespondWith`
[json|
@@ -590,7 +596,7 @@ spec = do
get "/complex_items?select=id::fakecolumntype"
`shouldRespondWith` [json| {"hint":null,"details":null,"code":"42704","message":"type \"fakecolumntype\" does not exist"} |]
{ matchStatus = 400
, matchHeaders = []
, matchHeaders = ["Content-Length" <:> "94"]
}
it "can cast types with underscore and numbers" $
@@ -603,7 +609,11 @@ spec = do
it "requesting parents and children" $
get "/projects?id=eq.1&select=id, name, clients(*), tasks(id, name)" `shouldRespondWith`
[json|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
{ matchHeaders = [matchContentTypeJson] }
{ matchHeaders = [
"Content-Length" <:> "141",
matchContentTypeJson
]
}
it "requesting parent and renaming primary key" $
get "/projects?select=name,client:clients(clientId:id,name)" `shouldRespondWith`
@@ -1145,7 +1155,10 @@ spec = do
get "/items?order=id.asc.nullslasttt" `shouldRespondWith`
[json|{"details":"unexpected 't' expecting \",\" or end of input","message":"\"failed to parse order (id.asc.nullslasttt)\" (line 1, column 17)","code":"PGRST100","hint":null}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
, matchHeaders = [
"Content-Length" <:> "169",
matchContentTypeJson
]
}
describe "Accept headers" $ do
@@ -1155,7 +1168,10 @@ spec = do
`shouldRespondWith`
[json|{"message":"None of these media types are available: text/unknowntype","code":"PGRST107","details":null,"hint":null}|]
{ matchStatus = 406
, matchHeaders = [matchContentTypeJson]
, matchHeaders = [
"Content-Length" <:> "116",
matchContentTypeJson
]
}
it "should respond correctly to */* in accept header" $
@@ -1194,7 +1210,10 @@ spec = do
(acceptHdrs "text/csv; version=1") ""
`shouldRespondWith` "k,extra\nxyyx,u\nxYYx,v"
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]
, matchHeaders = [
"Content-Type" <:> "text/csv; charset=utf-8",
"Content-Length" <:> "21"
]
}
describe "Canonical location" $ do
@@ -1512,7 +1531,10 @@ spec = do
get "/datarep_todos?select=id,label_color,banana" `shouldRespondWith`
[json| {"code":"42703","details":null,"hint":null,"message":"column datarep_todos.banana does not exist"} |]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
, matchHeaders = [
"Content-Length" <:> "98",
matchContentTypeJson
]
}
it "formats columns in views including computed columns" $
get "/datarep_todos_computed?select=id,label_color,dark_color" `shouldRespondWith`
@@ -1548,8 +1570,8 @@ spec = do
get "/datarep_todos?id=lt.4" `shouldRespondWith`
[json| [
{"id":1,"name":"Report","label_color":"#000000","due_at":"2018-01-02T00:00:00Z","icon_image":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQAAAAA3bvkkAAAAABBJREFUeJxiYAEAAAAA//8DAAAABgAFBXv6vUAAAAAASUVORK5CYII=","created_at":1513213350,"budget":"12.50"},
{"id":2,"name":"Essay","label_color":"#000100","due_at":"2018-01-03T00:00:00Z","icon_image":null,"created_at":1513213350,"budget":"100000000000000.13"},
{"id":3,"name":"Algebra","label_color":"#01E240","due_at":"2018-01-01T14:12:34.123456Z","icon_image":null,"created_at":1513213350,"budget":"0.00"}
{"id":2,"name":"Essay","label_color":"#000100","due_at":"2018-01-03T00:00:00Z","icon_image":null,"created_at":1513213350,"budget":"100000000000000.13"},
{"id":3,"name":"Algebra","label_color":"#01E240","due_at":"2018-01-01T14:12:34.123456Z","icon_image":null,"created_at":1513213350,"budget":"0.00"}
] |]
{ matchHeaders = [matchContentTypeJson] }
it "formats star and explicit mix" $
@@ -1580,7 +1602,10 @@ spec = do
-- we prove the parser is not used because it'd replace the Z with `+00:00` and a different error message.
[json| {"code":"22007","details":null,"hint":null,"message":"invalid input syntax for type timestamp with time zone: \"Z\""} |]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
, matchHeaders = [
"Content-Length" <:> "117",
matchContentTypeJson
]
}
it "uses text parser for filter with 'IN' predicates" $
get "/datarep_todos?select=id,due_at&label_color=in.(000100,01E240)" `shouldRespondWith`
@@ -1607,7 +1632,10 @@ spec = do
{"code":"42883","details":null,"hint":"No operator matches the given name and argument types. You might need to add explicit type casts.","message":"operator does not exist: public.color ~~* unknown"}
|]
{ matchStatus = 404
, matchHeaders = [matchContentTypeJson]
, matchHeaders = [
"Content-Length" <:> "200",
matchContentTypeJson
]
}
context "searching for an empty string" $ do
@@ -1624,11 +1652,13 @@ spec = do
it "return http status 500" $
get "/infinite_recursion?select=*" `shouldRespondWith`
[json|{"code":"42P17","message":"infinite recursion detected in rules for relation \"infinite_recursion\"","details":null,"hint":null}|]
{ matchStatus = 500 }
{ matchStatus = 500
, matchHeaders = ["Content-Length" <:> "128"] }
context "invalid resource path" $ do
it "return http status 404" $
get "/first/second/third?select=*"
`shouldRespondWith`
[json| {"code":"PGRST125","details":null,"hint":null,"message":"Invalid path specified in request URL"} |]
{ matchStatus = 404 }
{ matchStatus = 404
, matchHeaders = ["Content-Length" <:> "96"]}
+8 -4
View File
@@ -22,7 +22,10 @@ spec = do
context "when I don't want the count" $ do
it "returns range Content-Range with */* for empty range" $
get "/rpc/getitemrange?min=2&max=2"
`shouldRespondWith` [json| [] |] {matchHeaders = ["Content-Range" <:> "*/*"]}
`shouldRespondWith` [json| [] |]
{ matchHeaders = [ "Content-Range" <:> "*/*"
, "Content-Length" <:> "2" ]
}
it "returns range Content-Range with range/*" $
get "/rpc/getitemrange?order=id&min=0&max=15"
@@ -42,7 +45,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" $
@@ -65,8 +69,8 @@ spec = do
r <- request methodGet "/rpc/getitemrange?min=0&max=15"
(rangeHdrs $ ByteRangeFromTo 0 1) mempty
liftIO $ do
simpleHeaders r `shouldSatisfy`
matchHeader "Content-Range" "0-1/*"
simpleHeaders r `shouldSatisfy` matchHeader "Content-Range" "0-1/*"
simpleHeaders r `shouldSatisfy` matchHeader "Content-Length" "22"
simpleStatus r `shouldBe` ok200
it "understands open-ended ranges" $
+42 -16
View File
@@ -31,18 +31,21 @@ spec =
post "/rpc/getitemrange?limit=1&offset=1" [json| { "min": 2, "max": 4 } |]
`shouldRespondWith` [json| [{"id":4}] |]
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "1-1/*"]
, matchHeaders = [ "Content-Range" <:> "1-1/*"
, "Content-Length" <:> "10" ]
}
get "/rpc/getitemrange?min=2&max=4&limit=1&offset=1"
`shouldRespondWith` [json| [{"id":4}] |]
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "1-1/*"]
, matchHeaders = [ "Content-Range" <:> "1-1/*"
, "Content-Length" <:> "10"]
}
request methodHead "/rpc/getitemrange?min=2&max=4&limit=1&offset=1" mempty mempty
`shouldRespondWith`
""
{ matchStatus = 200
, matchHeaders = [ matchContentTypeJson
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "1-1/*" ]
}
@@ -210,7 +213,8 @@ spec =
"code":"PGRST202",
"details":"Searched for the function test.sayhell without parameters, but no matches were found in the schema cache."} |]
{ matchStatus = 404
, matchHeaders = [matchContentTypeJson]
, matchHeaders = [ "Content-Length" <:> "291"
, matchContentTypeJson ]
}
it "should fail with 404 on unknown proc args" $ do
@@ -268,7 +272,8 @@ spec =
"code":"PGRST203",
"details":null} |]
{ matchStatus = 300
, matchHeaders = [matchContentTypeJson]
, matchHeaders = [ "Content-Length" <:> "353"
, matchContentTypeJson ]
}
it "works when having uppercase identifiers" $ do
@@ -465,7 +470,8 @@ spec =
`shouldRespondWith`
""
{ matchStatus = 204
, matchHeaders = [matchHeaderAbsent hContentType]
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength]
}
it "returns null for an integer with null value" $
@@ -613,7 +619,8 @@ spec =
`shouldRespondWith`
[json|{"message":"Cannot use the DELETE method on RPC","code":"PGRST101","details":null,"hint":null}|]
{ matchStatus = 405
, matchHeaders = [matchContentTypeJson]
, matchHeaders = [ "Content-Length" <:> "94"
, matchContentTypeJson ]
}
it "PATCH fails" $
request methodPatch "/rpc/sayhello" [] ""
@@ -628,7 +635,8 @@ spec =
`shouldRespondWith`
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType ]
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength]
}
-- now the test
@@ -761,7 +769,8 @@ spec =
get "/rpc/raise_pt402"
`shouldRespondWith` [json|{ "hint": "Upgrade your plan", "details": "Quota exceeded", "code": "PT402", "message": "Payment Required" }|]
{ matchStatus = 402
, matchHeaders = [matchContentTypeJson]
, matchHeaders = [ "Content-Length" <:> "99"
, matchContentTypeJson ]
}
it "defaults to status 500 if RAISE code is PT not followed by a number" $
@@ -769,7 +778,8 @@ spec =
`shouldRespondWith`
[json|{"hint": null, "details": null, "code": "PT40A", "message": "Wrong"}|]
{ matchStatus = 500
, matchHeaders = [ matchContentTypeJson ]
, matchHeaders = [ "Content-Length" <:> "61"
, matchContentTypeJson ]
}
context "should work with an overloaded function" $ do
@@ -1024,7 +1034,8 @@ spec =
`shouldRespondWith`
[json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value","code":"PGRST111","details":null,"hint":null}|]
{ matchStatus = 500
, matchHeaders = [ matchContentTypeJson ]
, matchHeaders = [ "Content-Length" <:> "157"
, matchContentTypeJson ]
}
get "/rpc/bad_guc_headers_2"
`shouldRespondWith`
@@ -1051,6 +1062,7 @@ spec =
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Set-Cookie" <:> "sessionid=38afes7a8; HttpOnly; Path=/"
, "Set-Cookie" <:> "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly" ]
}
@@ -1113,7 +1125,8 @@ spec =
`shouldRespondWith`
[json|{"message":"response.status guc must be a valid status code","code":"PGRST112","details":null,"hint":null}|]
{ matchStatus = 500
, matchHeaders = [ matchContentTypeJson ]
, matchHeaders = [ "Content-Length" <:> "106"
, matchContentTypeJson ]
}
context "single unnamed param" $ do
@@ -1291,7 +1304,8 @@ spec =
"details":"line 1: StartTag: invalid element name\n<\n ^"
}|]
{ matchStatus = 400
, matchHeaders = [matchContentTypeJson]
, matchHeaders = [ "Content-Length" <:> "118"
, matchContentTypeJson ]
}
-- https://github.com/PostgREST/postgrest/issues/1586#issuecomment-696345442
@@ -1304,7 +1318,10 @@ spec =
post "/rpc/char_param_insert" [json| { "char_": "abcdefg", "char_arr": "{abc,abcdefg}" } |]
`shouldRespondWith`
[json| {"code":"22001","details":null,"hint":null,"message":"value too long for type character(5)"} |]
{ matchStatus = 400 }
{ matchStatus = 400
, matchHeaders = [ "Content-Length" <:> "92"
, matchContentTypeJson ]
}
it "modifies the param type from bit to bit varying" $ do
get "/rpc/bit_param_select?bit_=101010&bit_arr={101,101010}" `shouldRespondWith`
@@ -1314,7 +1331,10 @@ spec =
post "/rpc/bit_param_insert" [json| { "bit_": "101010", "bit_arr": "{101,101010}" } |]
`shouldRespondWith`
[json| {"code":"22026","details":null,"hint":null,"message":"bit string length 6 does not match type bit(5)"} |]
{ matchStatus = 400 }
{ matchStatus = 400
, matchHeaders = [ "Content-Length" <:> "102"
, matchContentTypeJson ]
}
context "get message and details from raise sqlstate" $ do
it "gets message and details from raise sqlstate PGRST" $ do
@@ -1369,7 +1389,10 @@ spec =
"message":"Could not parse JSON in the \"RAISE SQLSTATE 'PGRST'\" error",
"details":"Invalid JSON value for MESSAGE: 'INVALID'",
"hint":"MESSAGE must be a JSON object with obligatory keys: 'code', 'message' and optional keys: 'details', 'hint'."}|]
{ matchStatus = 500 }
{ matchStatus = 500
, matchHeaders = [ "Content-Length" <:> "263"
, matchContentTypeJson ]
}
it "returns error for invalid JSON in the DETAIL option of the RAISE statement" $
get "/rpc/raise_sqlstate_invalid_json_details" `shouldRespondWith`
@@ -1395,4 +1418,7 @@ spec =
it "should return http status 500" $
request methodGet "/rpc/temp_file_limit" [auth] "" `shouldRespondWith`
[json|{"code":"53400","message":"temporary file size exceeds temp_file_limit (1kB)","details":null,"hint":null}|]
{ matchStatus = 500 }
{ matchStatus = 500
, matchHeaders = [ "Content-Length" <:> "105"
, matchContentTypeJson ]
}
+25 -7
View File
@@ -20,7 +20,7 @@ spec = do
`shouldRespondWith`
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.factories'","message":"Could not find the table 'test.fake' in the schema cache"} |]
{ matchStatus = 404
, matchHeaders = []
, matchHeaders = ["Content-Length" <:> "157"]
}
@@ -31,7 +31,8 @@ spec = do
`shouldRespondWith`
""
{ matchStatus = 204,
matchHeaders = [matchHeaderAbsent hContentType]
matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength ]
}
context "with invalid json payload" $
@@ -40,7 +41,8 @@ spec = do
`shouldRespondWith`
[json|{"message":"Empty or invalid json","code":"PGRST102","details":null,"hint":null}|]
{ matchStatus = 400,
matchHeaders = [matchContentTypeJson]
matchHeaders = [ matchContentTypeJson
, "Content-Length" <:> "80"]
}
context "with no payload" $
@@ -59,6 +61,7 @@ spec = do
"\t \n \r { \"id\": 99 } \t \n \r "
`shouldRespondWith` [json|[{"id":99}]|]
{ matchStatus = 200
, matchHeaders = ["Content-Length" <:> "11"]
}
context "in a nonempty table" $ do
@@ -69,6 +72,7 @@ spec = do
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "0-0/*" ]
}
@@ -78,7 +82,8 @@ spec = do
`shouldRespondWith` "[]"
{
matchStatus = 200,
matchHeaders = ["Preference-Applied" <:> "return=representation"]
matchHeaders = [ "Preference-Applied" <:> "return=representation"
, "Content-Length" <:> "2"]
}
it "returns status code 200 when no rows updated" $
@@ -107,6 +112,7 @@ spec = do
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "0-1/*"
, "Preference-Applied" <:> "tx=commit" ]
}
@@ -123,7 +129,8 @@ spec = do
`shouldRespondWith`
""
{ matchStatus = 204
, matchHeaders = [matchHeaderAbsent hContentType]
, matchHeaders = [matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength ]
}
it "can set a column to NULL" $ do
@@ -207,6 +214,7 @@ spec = do
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "0-0/*"
, "Preference-Applied" <:> "return=minimal"]
}
@@ -217,6 +225,7 @@ spec = do
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "0-0/*"
, "Preference-Applied" <:> "return=minimal"]
}
@@ -230,6 +239,7 @@ spec = do
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "*/*" ]
}
@@ -240,6 +250,7 @@ spec = do
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "*/*" ]
}
@@ -250,6 +261,7 @@ spec = do
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "*/*" ]
}
@@ -261,6 +273,7 @@ spec = do
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "*/*" ]
}
@@ -271,6 +284,7 @@ spec = do
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "*/*" ]
}
@@ -281,6 +295,7 @@ spec = do
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "*/*" ]
}
@@ -350,7 +365,7 @@ spec = do
`shouldRespondWith`
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.articles'","message":"Could not find the table 'test.garlic' in the schema cache"} |]
{ matchStatus = 404
, matchHeaders = []
, matchHeaders = ["Content-Length" <:> "158"]
}
context "apply defaults on missing values" $ do
@@ -613,6 +628,7 @@ spec = do
""
{ matchStatus = 204
, matchHeaders = [matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Preference-Applied" <:> "return=minimal"]
}
@@ -623,7 +639,8 @@ spec = do
`shouldRespondWith`
""
{ matchStatus = 204
, matchHeaders = [matchHeaderAbsent hContentType]
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength ]
}
-- Data representations for payload parsing requires Postgres 10 or above.
@@ -636,6 +653,7 @@ spec = do
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "0-0/*" ]
}
+3 -2
View File
@@ -30,7 +30,9 @@ deleteItems =
`shouldRespondWith`
""
{ matchStatus = 204
, matchHeaders = [matchHeaderAbsent hContentType] }
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength ]
}
preferDefault = [("Prefer", "return=representation")]
preferCommit = [("Prefer", "return=representation"), ("Prefer", "tx=commit")]
@@ -262,4 +264,3 @@ forced = describe "tx-rollback-all = true, tx-allow-override = false" $ do
preferRollback `shouldRespondToReads` withoutPreferenceApplied
preferRollback `shouldNotPersistMutations` withoutPreferenceApplied
preferRollback `shouldRaiseExceptions` withoutPreferenceApplied
@@ -56,6 +56,7 @@ spec =
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "X-Custom-Header" <:> "mykey=myval" ]
}
@@ -66,6 +67,7 @@ spec =
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "X-Custom-Header" <:> "mykey=myval" ]
}
@@ -75,6 +77,7 @@ spec =
""
{ matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "X-Custom-Header" <:> "mykey=myval" ]
}
it "can override the Content-Type header" $ do
+3
View File
@@ -281,6 +281,9 @@ noBlankHeader = notElem mempty
noProfileHeader :: [Header] -> Bool
noProfileHeader headers = isNothing $ find ((== "Content-Profile") . fst) headers
notZeroContentLength :: [Header] -> Bool
notZeroContentLength headers = maybe False (/= "0") $ lookup hContentLength headers
authHeader :: BS.ByteString -> BS.ByteString -> Header
authHeader typ creds =
(hAuthorization, typ <> " " <> creds)