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 + The selected columns in the embedded resources are aggregated into arrays
+ Aggregates are not supported + Aggregates are not supported
- #2967, Add `Proxy-Status` header for better error response - @taimoorzaeem - #2967, Add `Proxy-Status` header for better error response - @taimoorzaeem
- #4016, Add `Content-Length` response header - @laurenceisla
### Fixed ### Fixed
+25 -4
View File
@@ -15,14 +15,14 @@ Observability allows measuring a system's current state based on the data it gen
Logs 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: With :ref:`log-level` set to ``info``, we get:
.. code:: .. 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 - 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 - "" "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``: For diagnostic information about the server itself, PostgREST logs to ``stderr``:
@@ -73,7 +73,7 @@ This will be logged by PostgREST:
.. code:: .. 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 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 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. 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: .. _explain_plan:
Execution plan Execution plan
+6 -2
View File
@@ -20,6 +20,7 @@ module PostgREST.Error
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as LBS
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import qualified Data.FuzzySet as Fuzzy import qualified Data.FuzzySet as Fuzzy
import qualified Data.HashMap.Strict as HM import qualified Data.HashMap.Strict as HM
@@ -59,8 +60,11 @@ class (ErrorBody a, JSON.ToJSON a) => PgrstError a where
errorResponseFor :: a -> Response errorResponseFor :: a -> Response
errorResponseFor err = errorResponseFor err =
let baseHeader = MediaType.toContentType MTApplicationJSON in let
responseLBS (status err) (baseHeader : headers err) $ errorPayload err 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 class ErrorBody a where
code :: a -> Text code :: a -> Text
+32 -15
View File
@@ -68,6 +68,7 @@ actionResponse (DbCrudResult WrappedReadPlan{wrMedia, wrHdrsOnly=headersOnly, cr
RSStandard{..} -> do RSStandard{..} -> do
let let
(status, contentRange) = RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal (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 [] prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing preferCount preferTransaction Nothing preferHandling preferTimezone Nothing []
headers = headers =
[ contentRange [ contentRange
@@ -77,6 +78,7 @@ actionResponse (DbCrudResult WrappedReadPlan{wrMedia, wrHdrsOnly=headersOnly, cr
<> if BS.null (qsCanonical iQueryParams) then mempty else "?" <> qsCanonical iQueryParams <> if BS.null (qsCanonical iQueryParams) then mempty else "?" <> qsCanonical iQueryParams
) )
] ]
++ cLHeader
++ contentTypeHeaders wrMedia ctxApiRequest ++ contentTypeHeaders wrMedia ctxApiRequest
++ prefHeader ++ prefHeader
@@ -90,7 +92,7 @@ actionResponse (DbCrudResult WrappedReadPlan{wrMedia, wrHdrsOnly=headersOnly, cr
Right $ PgrstResponse ovStatus ovHeaders bod Right $ PgrstResponse ovStatus ovHeaders bod
RSPlan plan -> 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 actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationCreate, mrMutatePlan, mrMedia, crudQi=QualifiedIdentifier{..}} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}, ..} _ _ _ _ _ = case resultSet of
RSStandard{..} -> do RSStandard{..} -> do
@@ -112,6 +114,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationCreate, mrMutateP
) )
, Just . RangeQuery.contentRangeH 1 0 $ , Just . RangeQuery.contentRangeH 1 0 $
if shouldCount preferCount then Just rsQueryTotal else Nothing if shouldCount preferCount then Just rsQueryTotal else Nothing
, Just $ contentLengthHeaderStrict rsBody
, prefHeader ] , prefHeader ]
let isInsertIfGTZero i = let isInsertIfGTZero i =
@@ -130,7 +133,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationCreate, mrMutateP
Right $ PgrstResponse ovStatus ovHeaders bod Right $ PgrstResponse ovStatus ovHeaders bod
RSPlan plan -> 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 actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationUpdate, mrMedia} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = case resultSet of
RSStandard{..} -> do RSStandard{..} -> do
@@ -143,7 +146,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationUpdate, mrMedia}
let (status, headers', body) = let (status, headers', body) =
case preferRepresentation of 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) Just None -> (HTTP.status204, headers, mempty)
_ -> (HTTP.status204, headers, mempty) _ -> (HTTP.status204, headers, mempty)
@@ -152,19 +155,20 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationUpdate, mrMedia}
Right $ PgrstResponse ovStatus ovHeaders body Right $ PgrstResponse ovStatus ovHeaders body
RSPlan plan -> 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 actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationSingleUpsert, mrMedia} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = case resultSet of
RSStandard {..} -> do RSStandard {..} -> do
let let
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation preferCount preferTransaction Nothing preferHandling preferTimezone Nothing [] prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation preferCount preferTransaction Nothing preferHandling preferTimezone Nothing []
cLHeader = [contentLengthHeaderStrict rsBody]
cTHeader = contentTypeHeaders mrMedia ctxApiRequest cTHeader = contentTypeHeaders mrMedia ctxApiRequest
let isInsertIfGTZero i = if i > 0 then HTTP.status201 else HTTP.status200 let isInsertIfGTZero i = if i > 0 then HTTP.status201 else HTTP.status200
upsertStatus = isInsertIfGTZero $ fromJust rsInserted upsertStatus = isInsertIfGTZero $ fromJust rsInserted
(status, headers, body) = (status, headers, body) =
case preferRepresentation of 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) Just None -> (HTTP.status204, prefHeader, mempty)
_ -> (HTTP.status204, prefHeader, mempty) _ -> (HTTP.status204, prefHeader, mempty)
(ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status headers (ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status headers
@@ -172,7 +176,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationSingleUpsert, mrM
Right $ PgrstResponse ovStatus ovHeaders body Right $ PgrstResponse ovStatus ovHeaders body
RSPlan plan -> 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 actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationDelete, mrMedia} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = case resultSet of
RSStandard {..} -> do RSStandard {..} -> do
@@ -185,7 +189,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationDelete, mrMedia}
let (status, headers', body) = let (status, headers', body) =
case preferRepresentation of 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) Just None -> (HTTP.status204, headers, mempty)
_ -> (HTTP.status204, headers, mempty) _ -> (HTTP.status204, headers, mempty)
@@ -194,7 +198,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationDelete, mrMedia}
Right $ PgrstResponse ovStatus ovHeaders body Right $ PgrstResponse ovStatus ovHeaders body
RSPlan plan -> 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 actionResponse (DbCallResult CallReadPlan{crMedia, crInvMthd=invMethod, crProc=proc} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..},..} _ _ _ _ _ = case resultSet of
RSStandard {..} -> do RSStandard {..} -> do
@@ -205,7 +209,9 @@ actionResponse (DbCallResult CallReadPlan{crMedia, crInvMthd=invMethod, crProc=p
then Error.errorPayload $ Error.ApiRequestError $ Error.InvalidRange then Error.errorPayload $ Error.ApiRequestError $ Error.InvalidRange
$ Error.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal) $ Error.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal)
else LBS.fromStrict rsBody else LBS.fromStrict rsBody
isHeadMethod = invMethod == InvRead True
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing preferCount preferTransaction Nothing preferHandling preferTimezone preferMaxAffected [] prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing preferCount preferTransaction Nothing preferHandling preferTimezone preferMaxAffected []
cLHeader = if isHeadMethod then mempty else [contentLengthHeaderLazy rsOrErrBody]
headers = contentRange : prefHeader headers = contentRange : prefHeader
let (status', headers', body) = let (status', headers', body) =
@@ -213,20 +219,22 @@ actionResponse (DbCallResult CallReadPlan{crMedia, crInvMthd=invMethod, crProc=p
(HTTP.status204, headers, mempty) (HTTP.status204, headers, mempty)
else else
(status, (status,
headers ++ contentTypeHeaders crMedia ctxApiRequest, headers ++ cLHeader ++ contentTypeHeaders crMedia ctxApiRequest,
if invMethod == InvRead True then mempty else rsOrErrBody) if isHeadMethod then mempty else rsOrErrBody)
(ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status' headers' (ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status' headers'
Right $ PgrstResponse ovStatus ovHeaders body Right $ PgrstResponse ovStatus ovHeaders body
RSPlan plan -> 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 = actionResponse (MaybeDbResult InspectPlan{ipHdrsOnly=headersOnly} body) _ versions conf sCache schema negotiatedByProfile =
Right $ PgrstResponse HTTP.status200 let
(MediaType.toContentType MTOpenAPI : maybeToList (profileHeader schema negotiatedByProfile)) rsBody = maybe mempty (\(x, y, z) -> if headersOnly then mempty else OpenAPI.encode versions conf sCache x y z) body
(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} _ _ = actionResponse (NoDbResult (RelInfoPlan qi@QualifiedIdentifier{..})) _ _ _ SchemaCache{dbTables} _ _ =
case HM.lookup qi dbTables of case HM.lookup qi dbTables of
@@ -251,7 +259,7 @@ actionResponse (NoDbResult SchemaInfoPlan) _ _ _ _ _ _ = respondInfo "OPTIONS,GE
respondInfo :: ByteString -> Either Error.Error PgrstResponse respondInfo :: ByteString -> Either Error.Error PgrstResponse
respondInfo allowHeader = respondInfo allowHeader =
let allOrigins = ("Access-Control-Allow-Origin", "*") in 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 -- 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]) 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 = decodeGucStatus =
maybe (Right Nothing) $ first (const . Error.ApiRequestError $ Error.GucStatusError) . fmap (Just . toEnum . fst) . decimal 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 -> [HTTP.Header]
contentTypeHeaders mediaType ApiRequest{..} = contentTypeHeaders mediaType ApiRequest{..} =
MediaType.toContentType mediaType : maybeToList (profileHeader iSchema iNegotiatedByProfile) 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 assert len(output) == 0
elif level == "error": elif level == "error":
assert re.match( assert re.match(
r'- - - \[.+\] "GET / HTTP/1.1" 500 - "" "python-requests/.+"', r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
output[0], output[0],
) )
assert len(output) == 1 assert len(output) == 1
elif level == "warn": elif level == "warn":
assert re.match( assert re.match(
r'- - - \[.+\] "GET / HTTP/1.1" 500 - "" "python-requests/.+"', r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
output[0], output[0],
) )
assert re.match( 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], output[1],
) )
assert len(output) == 2 assert len(output) == 2
elif level == "info": elif level == "info":
assert re.match( assert re.match(
r'- - - \[.+\] "GET / HTTP/1.1" 500 - "" "python-requests/.+"', r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
output[0], output[0],
) )
assert re.match( 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], output[1],
) )
assert re.match( 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], output[2],
) )
assert len(output) == 3 assert len(output) == 3
elif level == "debug": elif level == "debug":
assert re.match( assert re.match(
r'- - - \[.+\] "GET / HTTP/1.1" 500 - "" "python-requests/.+"', r'- - - \[.+\] "GET / HTTP/1.1" 500 \d+ "" "python-requests/.+"',
output[0], output[0],
) )
assert re.match( 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], output[1],
) )
assert re.match( 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], output[2],
) )
+5 -3
View File
@@ -22,7 +22,8 @@ spec = describe "authorization" $ do
"code":"42501", "code":"42501",
"message":"permission denied for table authors_only"} |] "message":"permission denied for table authors_only"} |]
{ matchStatus = 401 { 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" $ it "denies access to tables that postgrest_test_author does not own" $
@@ -35,7 +36,7 @@ spec = describe "authorization" $ do
"code":"42501", "code":"42501",
"message":"permission denied for table private_table"} |] "message":"permission denied for table private_table"} |]
{ matchStatus = 403 { matchStatus = 403
, matchHeaders = [] , matchHeaders = ["Content-Length" <:> "97"]
} }
it "denies execution on functions that anonymous does not own" $ it "denies execution on functions that anonymous does not own" $
@@ -92,7 +93,8 @@ spec = describe "authorization" $ do
{ matchStatus = 401 { matchStatus = 401
, matchHeaders = [ , matchHeaders = [
"WWW-Authenticate" <:> "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")] "" [("Accept","application/openapi+json")] ""
`shouldRespondWith` `shouldRespondWith`
[json| {"code":"PGRST126","details":null,"hint":null,"message":"Root endpoint metadata is disabled"} |] [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` `shouldRespondWith`
"" ""
{ matchStatus = 200 { 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 describe "table" $ do
+8 -4
View File
@@ -24,7 +24,8 @@ spec = describe "OpenAPI" $ do
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 200 { 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" $ it "should respond to openapi request on none root path with 406" $
@@ -38,11 +39,14 @@ spec = describe "OpenAPI" $ do
`shouldRespondWith` 406 `shouldRespondWith` 406
it "includes postgrest.org current version api docs" $ do 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 describe "schema" $ do
+3 -3
View File
@@ -15,9 +15,9 @@ spec = describe "Allow header" $ do
context "a table" $ do context "a table" $ do
it "includes read/write methods for writeable table" $ do it "includes read/write methods for writeable table" $ do
r <- request methodOptions "/items" [] "" r <- request methodOptions "/items" [] ""
liftIO $ liftIO $ do
simpleHeaders r `shouldSatisfy` simpleHeaders r `shouldSatisfy` matchHeader "Allow" "OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE"
matchHeader "Allow" "OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE" simpleHeaders r `shouldSatisfy` matchHeader "Content-Length" "0"
it "fails with 404 for an unknown table" $ it "fails with 404 for an unknown table" $
request methodOptions "/unknown" [] "" `shouldRespondWith` 404 request methodOptions "/unknown" [] "" `shouldRespondWith` 404
+29 -12
View File
@@ -20,6 +20,7 @@ spec = describe "custom media types" $ do
liftIO $ do liftIO $ do
simpleBody r `shouldBe` readFixtureFile "lines.twkb" simpleBody r `shouldBe` readFixtureFile "lines.twkb"
simpleHeaders r `shouldContain` [("Content-Type", "application/vnd.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 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") "" r <- request methodGet "/lines?id=eq.1" (acceptHdrs "application/vnd.twkb") ""
@@ -32,7 +33,8 @@ spec = describe "custom media types" $ do
`shouldRespondWith` `shouldRespondWith`
[json| {"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: text/plain"} |] [json| {"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: text/plain"} |]
{ matchStatus = 406 { 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 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` `shouldRespondWith`
"<myxml>foo</myxml>bar<foobar><baz/></foobar>" "<myxml>foo</myxml>bar<foobar><baz/></foobar>"
{ matchStatus = 200 { 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 -- TODO SOH (start of heading) is being added to results
@@ -50,7 +53,8 @@ spec = describe "custom media types" $ do
`shouldRespondWith` `shouldRespondWith`
"\SOH{\"type\": \"FeatureCollection\", \"hello\": \"world\"}" "\SOH{\"type\": \"FeatureCollection\", \"hello\": \"world\"}"
{ matchStatus = 200 { 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 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> |</html>
|] |]
{ matchStatus = 200 { 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 it "can get raw output with Accept: text/plain" $ do
request methodGet "/rpc/welcome" (acceptHdrs "text/plain") "" request methodGet "/rpc/welcome" (acceptHdrs "text/plain") ""
`shouldRespondWith` "Welcome to PostgREST" `shouldRespondWith` "Welcome to PostgREST"
{ matchStatus = 200 { 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 it "can get raw xml output with Accept: text/xml" $ do
@@ -98,7 +104,8 @@ spec = describe "custom media types" $ do
`shouldRespondWith` `shouldRespondWith`
"<my-xml-tag/>" "<my-xml-tag/>"
{ matchStatus = 200 { 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 it "can get raw xml output with Accept: text/xml" $ do
@@ -132,6 +139,7 @@ spec = describe "custom media types" $ do
liftIO $ do liftIO $ do
simpleBody r `shouldBe` readFixtureFile "A.png" simpleBody r `shouldBe` readFixtureFile "A.png"
simpleHeaders r `shouldContain` [("Content-Type", "image/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" $ context "Proc that returns set of scalars and Accept: text/plain" $
it "will err because only scalars work with media type domains" $ do it "will err because only scalars work with media type domains" $ do
@@ -141,7 +149,8 @@ spec = describe "custom media types" $ do
`shouldRespondWith` `shouldRespondWith`
[json|{"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: text/plain"}|] [json|{"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: text/plain"}|]
{ matchStatus = 406 { 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 context "Proc that returns rows and accepts custom media type" $ do
@@ -173,7 +182,8 @@ spec = describe "custom media types" $ do
`shouldRespondWith` `shouldRespondWith`
[json| {"overridden": "true"} |] [json| {"overridden": "true"} |]
{ matchStatus = 200 { 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 -- TODO SOH (start of heading) is being added to results
@@ -182,7 +192,8 @@ spec = describe "custom media types" $ do
`shouldRespondWith` `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\"}}]}" "\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 { 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" $ it "will not override vendored media types like application/vnd.pgrst.object" $
@@ -190,7 +201,8 @@ spec = describe "custom media types" $ do
`shouldRespondWith` `shouldRespondWith`
[json|{"id":1,"name":"Windows 7","client_id":1}|] [json|{"id":1,"name":"Windows 7","client_id":1}|]
{ matchStatus = 200 { 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 context "matches requested media type correctly" $ do
@@ -200,11 +212,13 @@ spec = describe "custom media types" $ do
liftIO $ do liftIO $ do
simpleBody r1 `shouldBe` readFixtureFile "A.png" simpleBody r1 `shouldBe` readFixtureFile "A.png"
simpleHeaders r1 `shouldContain` [("Content-Type", "image/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") "" r2 <- request methodGet "/rpc/ret_image" (acceptHdrs "text/html,application/xhtml+xml,application/xml;q=0.9,image/png,*/*;q=0.8") ""
liftIO $ do liftIO $ do
simpleBody r2 `shouldBe` readFixtureFile "A.png" simpleBody r2 `shouldBe` readFixtureFile "A.png"
simpleHeaders r2 `shouldContain` [("Content-Type", "image/png")] simpleHeaders r2 `shouldContain` [("Content-Type", "image/png")]
simpleHeaders r2 `shouldContain` [("Content-Length", "138")]
-- https://github.com/PostgREST/postgrest/issues/2170 -- https://github.com/PostgREST/postgrest/issues/2170
it "will match json in presence of text/plain" $ do it "will match json in presence of text/plain" $ do
@@ -219,7 +233,8 @@ spec = describe "custom media types" $ do
`shouldRespondWith` `shouldRespondWith`
"id\tname\tclient_id\n1\tWindows 7\t1\n2\tWindows 10\t1\n" "id\tname\tclient_id\n1\tWindows 7\t1\n2\tWindows 10\t1\n"
{ matchStatus = 200 { 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 -- 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" simpleBody r `shouldBe` readFixtureFile "lines.csv"
simpleHeaders r `shouldContain` [("Content-Type", "text/csv; charset=utf-8")] simpleHeaders r `shouldContain` [("Content-Type", "text/csv; charset=utf-8")]
simpleHeaders r `shouldContain` [("Content-Disposition", "attachment; filename=\"lines.csv\"")] simpleHeaders r `shouldContain` [("Content-Disposition", "attachment; filename=\"lines.csv\"")]
simpleHeaders r `shouldContain` [("Content-Length", "216")]
-- https://github.com/PostgREST/postgrest/issues/3160 -- https://github.com/PostgREST/postgrest/issues/3160
context "using select query parameter" $ do context "using select query parameter" $ do
@@ -239,7 +255,8 @@ spec = describe "custom media types" $ do
|(2,"Windows 10",1) |(2,"Windows 10",1)
|] |]
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Type" <:> "pg/outfunc"] , matchHeaders = [ "Content-Type" <:> "pg/outfunc"
, "Content-Length" <:> "37" ]
} }
it "with fewer columns selected" $ do it "with fewer columns selected" $ do
+9 -2
View File
@@ -22,6 +22,7 @@ spec =
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "*/*" ] , "Content-Range" <:> "*/*" ]
} }
@@ -30,6 +31,7 @@ spec =
`shouldRespondWith` [json|[{"id":2}]|] `shouldRespondWith` [json|[{"id":2}]|]
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/1" , matchHeaders = ["Content-Range" <:> "*/1"
, "Content-Length" <:> "10"
, "Preference-Applied" <:> "return=representation, count=exact"] , "Preference-Applied" <:> "return=representation, count=exact"]
} }
@@ -41,6 +43,7 @@ spec =
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "*/*" ] , "Content-Range" <:> "*/*" ]
} }
request methodDelete "/items?id=eq.3&select=id" request methodDelete "/items?id=eq.3&select=id"
@@ -50,6 +53,7 @@ spec =
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "*/*" , "Content-Range" <:> "*/*"
, "Preference-Applied" <:> "return=minimal"] , "Preference-Applied" <:> "return=minimal"]
} }
@@ -58,7 +62,8 @@ spec =
request methodDelete "/complex_items?id=eq.2&select=id,name" [("Prefer", "return=representation")] "" request methodDelete "/complex_items?id=eq.2&select=id,name" [("Prefer", "return=representation")] ""
`shouldRespondWith` [json|[{"id":2,"name":"Two"}]|] `shouldRespondWith` [json|[{"id":2,"name":"Two"}]|]
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/*"] , matchHeaders = [ "Content-Range" <:> "*/*"
, "Content-Length" <:> "23" ]
} }
it "can rename and cast the selected columns" $ it "can rename and cast the selected columns" $
@@ -137,6 +142,7 @@ spec =
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [matchHeaderAbsent hContentType , matchHeaders = [matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Preference-Applied" <:> "return=minimal" ] , "Preference-Applied" <:> "return=minimal" ]
} }
@@ -147,7 +153,8 @@ spec =
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [matchHeaderAbsent hContentType] , matchHeaders = [matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength]
} }
context "with ordering" $ context "with ordering" $
@@ -40,7 +40,8 @@ spec =
} }
|] |]
{ matchStatus = 300 { matchStatus = 300
, matchHeaders = [matchContentTypeJson] , matchHeaders = [ matchContentTypeJson
, "Content-Length" <:> "828" ]
} }
it "errs on an ambiguous embed that has a circular reference" $ it "errs on an ambiguous embed that has a circular reference" $
+12 -6
View File
@@ -26,7 +26,8 @@ pgErrorCodeMapping = do
"details": null, "details": null,
"hint": "Increase the configuration parameter \"max_stack_depth\" (currently 2048kB), after ensuring the platform's stack depth limit is adequate.", "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"}|] "message": "stack depth limit exceeded"}|]
{ matchStatus = 500 } { matchStatus = 500
, matchHeaders = ["Content-Length" <:> "217"] }
context "includes the proxy-status header on the response" $ do context "includes the proxy-status header on the response" $ do
it "works with ApiRequest error" $ it "works with ApiRequest error" $
@@ -34,7 +35,8 @@ pgErrorCodeMapping = do
`shouldRespondWith` `shouldRespondWith`
[json| {"code":"PGRST125","details":null,"hint":null,"message":"Invalid path specified in request URL"} |] [json| {"code":"PGRST125","details":null,"hint":null,"message":"Invalid path specified in request URL"} |]
{ matchStatus = 404 { matchStatus = 404
, matchHeaders = ["Proxy-Status" <:> "PostgREST; error=PGRST125"] , matchHeaders = [ "Proxy-Status" <:> "PostgREST; error=PGRST125"
, "Content-Length" <:> "96" ]
} }
it "works with SchemaCache error" $ it "works with SchemaCache error" $
@@ -42,7 +44,8 @@ pgErrorCodeMapping = do
`shouldRespondWith` `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"} |] [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 { matchStatus = 404
, matchHeaders = ["Proxy-Status" <:> "PostgREST; error=PGRST205"] , matchHeaders = [ "Proxy-Status" <:> "PostgREST; error=PGRST205"
, "Content-Length" <:> "172" ]
} }
it "works with Jwt error" $ do it "works with Jwt error" $ do
@@ -51,7 +54,8 @@ pgErrorCodeMapping = do
`shouldRespondWith` `shouldRespondWith`
[json| {"message":"Expected 3 parts in JWT; got 2","code":"PGRST301","hint":null,"details":null} |] [json| {"message":"Expected 3 parts in JWT; got 2","code":"PGRST301","hint":null,"details":null} |]
{ matchStatus = 401 { matchStatus = 401
, matchHeaders = ["Proxy-Status" <:> "PostgREST; error=PGRST301"] , matchHeaders = [ "Proxy-Status" <:> "PostgREST; error=PGRST301"
, "Content-Length" <:> "89" ]
} }
it "works with raise sqlstate custom error" $ it "works with raise sqlstate custom error" $
@@ -59,7 +63,8 @@ pgErrorCodeMapping = do
`shouldRespondWith` `shouldRespondWith`
[json| {"code":"PT402","details":"Quota exceeded","hint":"Upgrade your plan","message":"Payment Required"} |] [json| {"code":"PT402","details":"Quota exceeded","hint":"Upgrade your plan","message":"Payment Required"} |]
{ matchStatus = 402 { matchStatus = 402
, matchHeaders = ["Proxy-Status" <:> "PostgREST; error=PT402"] , matchHeaders = [ "Proxy-Status" <:> "PostgREST; error=PT402"
, "Content-Length" <:> "99" ]
} }
it "works with sqlstate PGRST custom error" $ it "works with sqlstate PGRST custom error" $
@@ -67,5 +72,6 @@ pgErrorCodeMapping = do
`shouldRespondWith` `shouldRespondWith`
[json| {"code":"123","details":"DEF","hint":"XYZ","message":"ABC"} |] [json| {"code":"123","details":"DEF","hint":"XYZ","message":"ABC"} |]
{ matchStatus = 332 { 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` "" } |] `shouldRespondWith` ""
{ matchStatus = 201 { matchStatus = 201
-- should not have content type set when body is empty -- 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" $ it "filters columns in result using &select" $
@@ -42,6 +43,7 @@ spec actualPgVersion = do
}] |] `shouldRespondWith` [json|[{"integer":14,"varchar":"testing!"}]|] }] |] `shouldRespondWith` [json|[{"integer":14,"varchar":"testing!"}]|]
{ matchStatus = 201 { matchStatus = 201
, matchHeaders = [matchContentTypeJson , matchHeaders = [matchContentTypeJson
, "Content-Length" <:> "37"
, "Preference-Applied" <:> "return=representation"] , "Preference-Applied" <:> "return=representation"]
} }
@@ -187,7 +189,8 @@ spec actualPgVersion = do
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType ] , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength ]
} }
request methodPost "/auto_incrementing_pk" 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"}|] [json|{"hint":null,"details":"Failing row contains (null, foo).","code":"23502","message":"null value in column \"k\" violates not-null constraint"}|]
) )
{ matchStatus = 400 { matchStatus = 400
, matchHeaders = [matchContentTypeJson] , matchHeaders = [ matchContentTypeJson]
} }
context "into a table with no pk" $ do context "into a table with no pk" $ do
@@ -286,7 +289,8 @@ spec actualPgVersion = do
`shouldRespondWith` `shouldRespondWith`
[json|{"message":"Empty or invalid json","code":"PGRST102","details":null,"hint":null}|] [json|{"message":"Empty or invalid json","code":"PGRST102","details":null,"hint":null}|]
{ matchStatus = 400 { matchStatus = 400
, matchHeaders = [matchContentTypeJson] , matchHeaders = [ matchContentTypeJson,
"Content-Length" <:> "80" ]
} }
context "with no payload" $ context "with no payload" $
@@ -370,7 +374,8 @@ spec actualPgVersion = do
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType ] , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength ]
} }
request methodPost "/items2" request methodPost "/items2"
@@ -388,7 +393,8 @@ spec actualPgVersion = do
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType ] , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength ]
} }
request methodPost "/items3?select=id" request methodPost "/items3?select=id"
@@ -623,7 +629,8 @@ spec actualPgVersion = do
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [matchHeaderAbsent hContentType] , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength ]
} }
describe "CSV insert" $ do describe "CSV insert" $ do
+10
View File
@@ -33,6 +33,7 @@ spec actualPgVersion = do
liftIO $ do liftIO $ do
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8") 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" } resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
totalCost `shouldBe` (if actualPgVersion >= pgVersion170 then 11.32 else 15.63) totalCost `shouldBe` (if actualPgVersion >= pgVersion170 then 11.32 else 15.63)
@@ -140,6 +141,7 @@ spec actualPgVersion = do
liftIO $ do liftIO $ do
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8") 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" } resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
totalCost `shouldBe` 0.06 totalCost `shouldBe` 0.06
@@ -153,6 +155,7 @@ spec actualPgVersion = do
liftIO $ do liftIO $ do
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8") 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" } resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
totalCost `shouldBe` 8.23 totalCost `shouldBe` 8.23
@@ -166,6 +169,7 @@ spec actualPgVersion = do
liftIO $ do liftIO $ do
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8") 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" } resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
totalCost `shouldBe` (if actualPgVersion >= pgVersion170 then 11.37 else 15.68) totalCost `shouldBe` (if actualPgVersion >= pgVersion170 then 11.37 else 15.68)
@@ -180,6 +184,7 @@ spec actualPgVersion = do
liftIO $ do liftIO $ do
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8") 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" } resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
totalCost `shouldBe` 3.55 totalCost `shouldBe` 3.55
@@ -194,6 +199,7 @@ spec actualPgVersion = do
liftIO $ do liftIO $ do
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8") 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" } resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
totalCost `shouldBe` 5.53 totalCost `shouldBe` 5.53
@@ -248,6 +254,7 @@ spec actualPgVersion = do
liftIO $ 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` 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)" |] aggCol `shouldBe` Just [aesonQQ| "COALESCE((json_agg(ROW(projects.id, projects.name, projects.client_id)) -> 0), 'null'::json)" |]
describe "function plan" $ do describe "function plan" $ do
@@ -261,6 +268,7 @@ spec actualPgVersion = do
liftIO $ do liftIO $ do
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; charset=utf-8") 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" } resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
totalCost `shouldBe` 68.56 totalCost `shouldBe` 68.56
@@ -275,6 +283,7 @@ spec actualPgVersion = do
liftIO $ do liftIO $ do
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+text; for=\"application/json\"; charset=utf-8") 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" } resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
resBody `shouldSatisfy` (\t -> LBS.take 9 t == "Aggregate") resBody `shouldSatisfy` (\t -> LBS.take 9 t == "Aggregate")
@@ -288,6 +297,7 @@ spec actualPgVersion = do
liftIO $ do liftIO $ do
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+text; for=\"application/json\"; charset=utf-8") 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" } resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
resBody `shouldSatisfy` (\t -> LBS.take 9 t == "Aggregate") resBody `shouldSatisfy` (\t -> LBS.take 9 t == "Aggregate")
+45 -15
View File
@@ -28,14 +28,17 @@ spec = do
`shouldRespondWith` `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"} |] [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 { matchStatus = 404
, matchHeaders = [] , matchHeaders = ["Content-Length" <:> "166"]
} }
describe "Filtering response" $ do describe "Filtering response" $ do
it "matches with equality" $ it "matches with equality" $
get "/items?id=eq.5" get "/items?id=eq.5"
`shouldRespondWith` [json| [{"id":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" $ it "matches with equality using not operator" $
get "/items?id=not.eq.5&order=id" get "/items?id=not.eq.5&order=id"
@@ -511,7 +514,10 @@ spec = do
"code":"PGRST108", "code":"PGRST108",
"message":"'non_existent_projects' is not an embedded resource in this request"}|] "message":"'non_existent_projects' is not an embedded resource in this request"}|]
{ matchStatus = 400 { matchStatus = 400
, matchHeaders = [matchContentTypeJson] , matchHeaders = [
"Content-Length" <:> "204",
matchContentTypeJson
]
} }
get "/clients?select=*,amiga_projects:projects(*)&amiga_projectsss.name=ilike.*Amiga*" `shouldRespondWith` get "/clients?select=*,amiga_projects:projects(*)&amiga_projectsss.name=ilike.*Amiga*" `shouldRespondWith`
[json| [json|
@@ -590,7 +596,7 @@ spec = do
get "/complex_items?select=id::fakecolumntype" get "/complex_items?select=id::fakecolumntype"
`shouldRespondWith` [json| {"hint":null,"details":null,"code":"42704","message":"type \"fakecolumntype\" does not exist"} |] `shouldRespondWith` [json| {"hint":null,"details":null,"code":"42704","message":"type \"fakecolumntype\" does not exist"} |]
{ matchStatus = 400 { matchStatus = 400
, matchHeaders = [] , matchHeaders = ["Content-Length" <:> "94"]
} }
it "can cast types with underscore and numbers" $ it "can cast types with underscore and numbers" $
@@ -603,7 +609,11 @@ spec = do
it "requesting parents and children" $ it "requesting parents and children" $
get "/projects?id=eq.1&select=id, name, clients(*), tasks(id, name)" `shouldRespondWith` 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"}]}]|] [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" $ it "requesting parent and renaming primary key" $
get "/projects?select=name,client:clients(clientId:id,name)" `shouldRespondWith` get "/projects?select=name,client:clients(clientId:id,name)" `shouldRespondWith`
@@ -1145,7 +1155,10 @@ spec = do
get "/items?order=id.asc.nullslasttt" `shouldRespondWith` 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}|] [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 { matchStatus = 400
, matchHeaders = [matchContentTypeJson] , matchHeaders = [
"Content-Length" <:> "169",
matchContentTypeJson
]
} }
describe "Accept headers" $ do describe "Accept headers" $ do
@@ -1155,7 +1168,10 @@ spec = do
`shouldRespondWith` `shouldRespondWith`
[json|{"message":"None of these media types are available: text/unknowntype","code":"PGRST107","details":null,"hint":null}|] [json|{"message":"None of these media types are available: text/unknowntype","code":"PGRST107","details":null,"hint":null}|]
{ matchStatus = 406 { matchStatus = 406
, matchHeaders = [matchContentTypeJson] , matchHeaders = [
"Content-Length" <:> "116",
matchContentTypeJson
]
} }
it "should respond correctly to */* in accept header" $ it "should respond correctly to */* in accept header" $
@@ -1194,7 +1210,10 @@ spec = do
(acceptHdrs "text/csv; version=1") "" (acceptHdrs "text/csv; version=1") ""
`shouldRespondWith` "k,extra\nxyyx,u\nxYYx,v" `shouldRespondWith` "k,extra\nxyyx,u\nxYYx,v"
{ matchStatus = 200 { 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 describe "Canonical location" $ do
@@ -1512,7 +1531,10 @@ spec = do
get "/datarep_todos?select=id,label_color,banana" `shouldRespondWith` get "/datarep_todos?select=id,label_color,banana" `shouldRespondWith`
[json| {"code":"42703","details":null,"hint":null,"message":"column datarep_todos.banana does not exist"} |] [json| {"code":"42703","details":null,"hint":null,"message":"column datarep_todos.banana does not exist"} |]
{ matchStatus = 400 { matchStatus = 400
, matchHeaders = [matchContentTypeJson] , matchHeaders = [
"Content-Length" <:> "98",
matchContentTypeJson
]
} }
it "formats columns in views including computed columns" $ it "formats columns in views including computed columns" $
get "/datarep_todos_computed?select=id,label_color,dark_color" `shouldRespondWith` get "/datarep_todos_computed?select=id,label_color,dark_color" `shouldRespondWith`
@@ -1548,8 +1570,8 @@ spec = do
get "/datarep_todos?id=lt.4" `shouldRespondWith` get "/datarep_todos?id=lt.4" `shouldRespondWith`
[json| [ [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":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":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":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] } { matchHeaders = [matchContentTypeJson] }
it "formats star and explicit mix" $ 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. -- 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\""} |] [json| {"code":"22007","details":null,"hint":null,"message":"invalid input syntax for type timestamp with time zone: \"Z\""} |]
{ matchStatus = 400 { matchStatus = 400
, matchHeaders = [matchContentTypeJson] , matchHeaders = [
"Content-Length" <:> "117",
matchContentTypeJson
]
} }
it "uses text parser for filter with 'IN' predicates" $ it "uses text parser for filter with 'IN' predicates" $
get "/datarep_todos?select=id,due_at&label_color=in.(000100,01E240)" `shouldRespondWith` 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"} {"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 { matchStatus = 404
, matchHeaders = [matchContentTypeJson] , matchHeaders = [
"Content-Length" <:> "200",
matchContentTypeJson
]
} }
context "searching for an empty string" $ do context "searching for an empty string" $ do
@@ -1624,11 +1652,13 @@ spec = do
it "return http status 500" $ it "return http status 500" $
get "/infinite_recursion?select=*" `shouldRespondWith` get "/infinite_recursion?select=*" `shouldRespondWith`
[json|{"code":"42P17","message":"infinite recursion detected in rules for relation \"infinite_recursion\"","details":null,"hint":null}|] [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 context "invalid resource path" $ do
it "return http status 404" $ it "return http status 404" $
get "/first/second/third?select=*" get "/first/second/third?select=*"
`shouldRespondWith` `shouldRespondWith`
[json| {"code":"PGRST125","details":null,"hint":null,"message":"Invalid path specified in request URL"} |] [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 context "when I don't want the count" $ do
it "returns range Content-Range with */* for empty range" $ it "returns range Content-Range with */* for empty range" $
get "/rpc/getitemrange?min=2&max=2" 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/*" $ it "returns range Content-Range with range/*" $
get "/rpc/getitemrange?order=id&min=0&max=15" get "/rpc/getitemrange?order=id&min=0&max=15"
@@ -42,7 +45,8 @@ spec = do
"hint":null "hint":null
}|] }|]
{ matchStatus = 416 { matchStatus = 416
, matchHeaders = ["Content-Range" <:> "*/0"] , matchHeaders = [ "Content-Range" <:> "*/0"
, "Content-Length" <:> "144"]
} }
it "refuses a range requesting start past last item" $ it "refuses a range requesting start past last item" $
@@ -65,8 +69,8 @@ spec = do
r <- request methodGet "/rpc/getitemrange?min=0&max=15" r <- request methodGet "/rpc/getitemrange?min=0&max=15"
(rangeHdrs $ ByteRangeFromTo 0 1) mempty (rangeHdrs $ ByteRangeFromTo 0 1) mempty
liftIO $ do liftIO $ do
simpleHeaders r `shouldSatisfy` simpleHeaders r `shouldSatisfy` matchHeader "Content-Range" "0-1/*"
matchHeader "Content-Range" "0-1/*" simpleHeaders r `shouldSatisfy` matchHeader "Content-Length" "22"
simpleStatus r `shouldBe` ok200 simpleStatus r `shouldBe` ok200
it "understands open-ended ranges" $ 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 } |] post "/rpc/getitemrange?limit=1&offset=1" [json| { "min": 2, "max": 4 } |]
`shouldRespondWith` [json| [{"id":4}] |] `shouldRespondWith` [json| [{"id":4}] |]
{ matchStatus = 200 { 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" get "/rpc/getitemrange?min=2&max=4&limit=1&offset=1"
`shouldRespondWith` [json| [{"id":4}] |] `shouldRespondWith` [json| [{"id":4}] |]
{ matchStatus = 200 { 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 request methodHead "/rpc/getitemrange?min=2&max=4&limit=1&offset=1" mempty mempty
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = [ matchContentTypeJson , matchHeaders = [ matchContentTypeJson
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "1-1/*" ] , "Content-Range" <:> "1-1/*" ]
} }
@@ -210,7 +213,8 @@ spec =
"code":"PGRST202", "code":"PGRST202",
"details":"Searched for the function test.sayhell without parameters, but no matches were found in the schema cache."} |] "details":"Searched for the function test.sayhell without parameters, but no matches were found in the schema cache."} |]
{ matchStatus = 404 { matchStatus = 404
, matchHeaders = [matchContentTypeJson] , matchHeaders = [ "Content-Length" <:> "291"
, matchContentTypeJson ]
} }
it "should fail with 404 on unknown proc args" $ do it "should fail with 404 on unknown proc args" $ do
@@ -268,7 +272,8 @@ spec =
"code":"PGRST203", "code":"PGRST203",
"details":null} |] "details":null} |]
{ matchStatus = 300 { matchStatus = 300
, matchHeaders = [matchContentTypeJson] , matchHeaders = [ "Content-Length" <:> "353"
, matchContentTypeJson ]
} }
it "works when having uppercase identifiers" $ do it "works when having uppercase identifiers" $ do
@@ -465,7 +470,8 @@ spec =
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [matchHeaderAbsent hContentType] , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength]
} }
it "returns null for an integer with null value" $ it "returns null for an integer with null value" $
@@ -613,7 +619,8 @@ spec =
`shouldRespondWith` `shouldRespondWith`
[json|{"message":"Cannot use the DELETE method on RPC","code":"PGRST101","details":null,"hint":null}|] [json|{"message":"Cannot use the DELETE method on RPC","code":"PGRST101","details":null,"hint":null}|]
{ matchStatus = 405 { matchStatus = 405
, matchHeaders = [matchContentTypeJson] , matchHeaders = [ "Content-Length" <:> "94"
, matchContentTypeJson ]
} }
it "PATCH fails" $ it "PATCH fails" $
request methodPatch "/rpc/sayhello" [] "" request methodPatch "/rpc/sayhello" [] ""
@@ -628,7 +635,8 @@ spec =
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType ] , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength]
} }
-- now the test -- now the test
@@ -761,7 +769,8 @@ spec =
get "/rpc/raise_pt402" get "/rpc/raise_pt402"
`shouldRespondWith` [json|{ "hint": "Upgrade your plan", "details": "Quota exceeded", "code": "PT402", "message": "Payment Required" }|] `shouldRespondWith` [json|{ "hint": "Upgrade your plan", "details": "Quota exceeded", "code": "PT402", "message": "Payment Required" }|]
{ matchStatus = 402 { matchStatus = 402
, matchHeaders = [matchContentTypeJson] , matchHeaders = [ "Content-Length" <:> "99"
, matchContentTypeJson ]
} }
it "defaults to status 500 if RAISE code is PT not followed by a number" $ it "defaults to status 500 if RAISE code is PT not followed by a number" $
@@ -769,7 +778,8 @@ spec =
`shouldRespondWith` `shouldRespondWith`
[json|{"hint": null, "details": null, "code": "PT40A", "message": "Wrong"}|] [json|{"hint": null, "details": null, "code": "PT40A", "message": "Wrong"}|]
{ matchStatus = 500 { matchStatus = 500
, matchHeaders = [ matchContentTypeJson ] , matchHeaders = [ "Content-Length" <:> "61"
, matchContentTypeJson ]
} }
context "should work with an overloaded function" $ do context "should work with an overloaded function" $ do
@@ -1024,7 +1034,8 @@ spec =
`shouldRespondWith` `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}|] [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 { matchStatus = 500
, matchHeaders = [ matchContentTypeJson ] , matchHeaders = [ "Content-Length" <:> "157"
, matchContentTypeJson ]
} }
get "/rpc/bad_guc_headers_2" get "/rpc/bad_guc_headers_2"
`shouldRespondWith` `shouldRespondWith`
@@ -1051,6 +1062,7 @@ spec =
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "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" ]
} }
@@ -1113,7 +1125,8 @@ spec =
`shouldRespondWith` `shouldRespondWith`
[json|{"message":"response.status guc must be a valid status code","code":"PGRST112","details":null,"hint":null}|] [json|{"message":"response.status guc must be a valid status code","code":"PGRST112","details":null,"hint":null}|]
{ matchStatus = 500 { matchStatus = 500
, matchHeaders = [ matchContentTypeJson ] , matchHeaders = [ "Content-Length" <:> "106"
, matchContentTypeJson ]
} }
context "single unnamed param" $ do context "single unnamed param" $ do
@@ -1291,7 +1304,8 @@ spec =
"details":"line 1: StartTag: invalid element name\n<\n ^" "details":"line 1: StartTag: invalid element name\n<\n ^"
}|] }|]
{ matchStatus = 400 { matchStatus = 400
, matchHeaders = [matchContentTypeJson] , matchHeaders = [ "Content-Length" <:> "118"
, matchContentTypeJson ]
} }
-- https://github.com/PostgREST/postgrest/issues/1586#issuecomment-696345442 -- 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}" } |] post "/rpc/char_param_insert" [json| { "char_": "abcdefg", "char_arr": "{abc,abcdefg}" } |]
`shouldRespondWith` `shouldRespondWith`
[json| {"code":"22001","details":null,"hint":null,"message":"value too long for type character(5)"} |] [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 it "modifies the param type from bit to bit varying" $ do
get "/rpc/bit_param_select?bit_=101010&bit_arr={101,101010}" `shouldRespondWith` 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}" } |] post "/rpc/bit_param_insert" [json| { "bit_": "101010", "bit_arr": "{101,101010}" } |]
`shouldRespondWith` `shouldRespondWith`
[json| {"code":"22026","details":null,"hint":null,"message":"bit string length 6 does not match type bit(5)"} |] [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 context "get message and details from raise sqlstate" $ do
it "gets message and details from raise sqlstate PGRST" $ 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", "message":"Could not parse JSON in the \"RAISE SQLSTATE 'PGRST'\" error",
"details":"Invalid JSON value for MESSAGE: 'INVALID'", "details":"Invalid JSON value for MESSAGE: 'INVALID'",
"hint":"MESSAGE must be a JSON object with obligatory keys: 'code', 'message' and optional keys: 'details', 'hint'."}|] "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" $ it "returns error for invalid JSON in the DETAIL option of the RAISE statement" $
get "/rpc/raise_sqlstate_invalid_json_details" `shouldRespondWith` get "/rpc/raise_sqlstate_invalid_json_details" `shouldRespondWith`
@@ -1395,4 +1418,7 @@ spec =
it "should return http status 500" $ it "should return http status 500" $
request methodGet "/rpc/temp_file_limit" [auth] "" `shouldRespondWith` request methodGet "/rpc/temp_file_limit" [auth] "" `shouldRespondWith`
[json|{"code":"53400","message":"temporary file size exceeds temp_file_limit (1kB)","details":null,"hint":null}|] [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` `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"} |] [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 { matchStatus = 404
, matchHeaders = [] , matchHeaders = ["Content-Length" <:> "157"]
} }
@@ -31,7 +31,8 @@ spec = do
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 204, { matchStatus = 204,
matchHeaders = [matchHeaderAbsent hContentType] matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength ]
} }
context "with invalid json payload" $ context "with invalid json payload" $
@@ -40,7 +41,8 @@ spec = do
`shouldRespondWith` `shouldRespondWith`
[json|{"message":"Empty or invalid json","code":"PGRST102","details":null,"hint":null}|] [json|{"message":"Empty or invalid json","code":"PGRST102","details":null,"hint":null}|]
{ matchStatus = 400, { matchStatus = 400,
matchHeaders = [matchContentTypeJson] matchHeaders = [ matchContentTypeJson
, "Content-Length" <:> "80"]
} }
context "with no payload" $ context "with no payload" $
@@ -59,6 +61,7 @@ spec = do
"\t \n \r { \"id\": 99 } \t \n \r " "\t \n \r { \"id\": 99 } \t \n \r "
`shouldRespondWith` [json|[{"id":99}]|] `shouldRespondWith` [json|[{"id":99}]|]
{ matchStatus = 200 { matchStatus = 200
, matchHeaders = ["Content-Length" <:> "11"]
} }
context "in a nonempty table" $ do context "in a nonempty table" $ do
@@ -69,6 +72,7 @@ spec = do
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "0-0/*" ] , "Content-Range" <:> "0-0/*" ]
} }
@@ -78,7 +82,8 @@ spec = do
`shouldRespondWith` "[]" `shouldRespondWith` "[]"
{ {
matchStatus = 200, matchStatus = 200,
matchHeaders = ["Preference-Applied" <:> "return=representation"] matchHeaders = [ "Preference-Applied" <:> "return=representation"
, "Content-Length" <:> "2"]
} }
it "returns status code 200 when no rows updated" $ it "returns status code 200 when no rows updated" $
@@ -107,6 +112,7 @@ spec = do
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "0-1/*" , "Content-Range" <:> "0-1/*"
, "Preference-Applied" <:> "tx=commit" ] , "Preference-Applied" <:> "tx=commit" ]
} }
@@ -123,7 +129,8 @@ spec = do
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [matchHeaderAbsent hContentType] , matchHeaders = [matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength ]
} }
it "can set a column to NULL" $ do it "can set a column to NULL" $ do
@@ -207,6 +214,7 @@ spec = do
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "0-0/*" , "Content-Range" <:> "0-0/*"
, "Preference-Applied" <:> "return=minimal"] , "Preference-Applied" <:> "return=minimal"]
} }
@@ -217,6 +225,7 @@ spec = do
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "0-0/*" , "Content-Range" <:> "0-0/*"
, "Preference-Applied" <:> "return=minimal"] , "Preference-Applied" <:> "return=minimal"]
} }
@@ -230,6 +239,7 @@ spec = do
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "*/*" ] , "Content-Range" <:> "*/*" ]
} }
@@ -240,6 +250,7 @@ spec = do
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "*/*" ] , "Content-Range" <:> "*/*" ]
} }
@@ -250,6 +261,7 @@ spec = do
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "*/*" ] , "Content-Range" <:> "*/*" ]
} }
@@ -261,6 +273,7 @@ spec = do
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "*/*" ] , "Content-Range" <:> "*/*" ]
} }
@@ -271,6 +284,7 @@ spec = do
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "*/*" ] , "Content-Range" <:> "*/*" ]
} }
@@ -281,6 +295,7 @@ spec = do
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "*/*" ] , "Content-Range" <:> "*/*" ]
} }
@@ -350,7 +365,7 @@ spec = do
`shouldRespondWith` `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"} |] [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 { matchStatus = 404
, matchHeaders = [] , matchHeaders = ["Content-Length" <:> "158"]
} }
context "apply defaults on missing values" $ do context "apply defaults on missing values" $ do
@@ -613,6 +628,7 @@ spec = do
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [matchHeaderAbsent hContentType , matchHeaders = [matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Preference-Applied" <:> "return=minimal"] , "Preference-Applied" <:> "return=minimal"]
} }
@@ -623,7 +639,8 @@ spec = do
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [matchHeaderAbsent hContentType] , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength ]
} }
-- Data representations for payload parsing requires Postgres 10 or above. -- Data representations for payload parsing requires Postgres 10 or above.
@@ -636,6 +653,7 @@ spec = do
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "Content-Range" <:> "0-0/*" ] , "Content-Range" <:> "0-0/*" ]
} }
+3 -2
View File
@@ -30,7 +30,9 @@ deleteItems =
`shouldRespondWith` `shouldRespondWith`
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [matchHeaderAbsent hContentType] } , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength ]
}
preferDefault = [("Prefer", "return=representation")] preferDefault = [("Prefer", "return=representation")]
preferCommit = [("Prefer", "return=representation"), ("Prefer", "tx=commit")] 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 `shouldRespondToReads` withoutPreferenceApplied
preferRollback `shouldNotPersistMutations` withoutPreferenceApplied preferRollback `shouldNotPersistMutations` withoutPreferenceApplied
preferRollback `shouldRaiseExceptions` withoutPreferenceApplied preferRollback `shouldRaiseExceptions` withoutPreferenceApplied
@@ -56,6 +56,7 @@ spec =
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "X-Custom-Header" <:> "mykey=myval" ] , "X-Custom-Header" <:> "mykey=myval" ]
} }
@@ -66,6 +67,7 @@ spec =
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "X-Custom-Header" <:> "mykey=myval" ] , "X-Custom-Header" <:> "mykey=myval" ]
} }
@@ -75,6 +77,7 @@ spec =
"" ""
{ matchStatus = 204 { matchStatus = 204
, matchHeaders = [ matchHeaderAbsent hContentType , matchHeaders = [ matchHeaderAbsent hContentType
, matchHeaderAbsent hContentLength
, "X-Custom-Header" <:> "mykey=myval" ] , "X-Custom-Header" <:> "mykey=myval" ]
} }
it "can override the Content-Type header" $ do it "can override the Content-Type header" $ do
+3
View File
@@ -281,6 +281,9 @@ noBlankHeader = notElem mempty
noProfileHeader :: [Header] -> Bool noProfileHeader :: [Header] -> Bool
noProfileHeader headers = isNothing $ find ((== "Content-Profile") . fst) headers 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 :: BS.ByteString -> BS.ByteString -> Header
authHeader typ creds = authHeader typ creds =
(hAuthorization, typ <> " " <> creds) (hAuthorization, typ <> " " <> creds)