Accept text/plain and text/html for raw output (#1330)

This commit is contained in:
Steve Chávez
2019-06-21 11:51:54 -05:00
committed by GitHub
parent 40ae7ce2b1
commit ea7d747107
8 changed files with 162 additions and 103 deletions
+1
View File
@@ -14,6 +14,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #1278, Add db-pool-timeout config option - @qu4tro - #1278, Add db-pool-timeout config option - @qu4tro
- #1285, Abort on wrong database password - @qu4tro - #1285, Abort on wrong database password - @qu4tro
- #790, Allow override of OpenAPI spec through `root-spec` config option - @steve-chavez - #790, Allow override of OpenAPI spec through `root-spec` config option - @steve-chavez
- #1308, Accept `text/plain` and `text/html` for raw output - @steve-chavez
### Fixed ### Fixed
+10 -28
View File
@@ -14,17 +14,16 @@ module PostgREST.ApiRequest (
, userApiRequest , userApiRequest
) where ) where
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import qualified Data.ByteString.Internal as BS (c2w) import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Lazy as BL import qualified Data.CaseInsensitive as CI
import qualified Data.CaseInsensitive as CI import qualified Data.Csv as CSV
import qualified Data.Csv as CSV import qualified Data.HashMap.Strict as M
import qualified Data.HashMap.Strict as M import qualified Data.List as L
import qualified Data.List as L import qualified Data.Set as S
import qualified Data.Set as S import qualified Data.Text as T
import qualified Data.Text as T import qualified Data.Vector as V
import qualified Data.Vector as V
import Control.Arrow ((***)) import Control.Arrow ((***))
import Data.Aeson.Types (emptyArray, emptyObject) import Data.Aeson.Types (emptyArray, emptyObject)
@@ -264,23 +263,6 @@ mutuallyAgreeable sProduces cAccepts =
then listToMaybe sProduces then listToMaybe sProduces
else exact else exact
-- PRIVATE ---------------------------------------------------------------
{-|
Warning: discards MIME parameters
-}
decodeContentType :: BS.ByteString -> ContentType
decodeContentType ct =
case BS.takeWhile (/= BS.c2w ';') ct of
"application/json" -> CTApplicationJSON
"text/csv" -> CTTextCSV
"application/openapi+json" -> CTOpenAPI
"application/vnd.pgrst.object+json" -> CTSingularJSON
"application/vnd.pgrst.object" -> CTSingularJSON
"application/octet-stream" -> CTOctetStream
"*/*" -> CTAny
ct' -> CTOther ct'
type CsvData = V.Vector (M.HashMap Text BL.ByteString) type CsvData = V.Vector (M.HashMap Text BL.ByteString)
{-| {-|
+16 -14
View File
@@ -39,7 +39,7 @@ import PostgREST.DbRequestBuilder (fieldNames, mutateRequest,
readRequest) readRequest)
import PostgREST.DbStructure import PostgREST.DbStructure
import PostgREST.Error (PgError (..), SimpleError (..), import PostgREST.Error (PgError (..), SimpleError (..),
errorResponseFor) errorResponseFor, singularityError)
import PostgREST.Middleware import PostgREST.Middleware
import PostgREST.OpenAPI import PostgREST.OpenAPI
import PostgREST.Parsers (pRequestColumns) import PostgREST.Parsers (pRequestColumns)
@@ -275,7 +275,7 @@ app dbStructure proc cols conf apiRequest =
callProc qi (specifiedProcArgs cols proc) returnsScalar q cq shouldCount callProc qi (specifiedProcArgs cols proc) returnsScalar q cq shouldCount
singular (iPreferSingleObjectParameter apiRequest) singular (iPreferSingleObjectParameter apiRequest)
(contentType == CTTextCSV) (contentType == CTTextCSV)
(contentType == CTOctetStream) bField (contentType `elem` rawContentTypes) bField
(pgVersion dbStructure) (pgVersion dbStructure)
let (tableTotal, queryTotal, body, jsonHeaders) = let (tableTotal, queryTotal, body, jsonHeaders) =
fromMaybe (Just 0, 0, "[]", "[]") row fromMaybe (Just 0, 0, "[]", "[]") row
@@ -334,11 +334,12 @@ responseContentTypeOrError :: [ContentType] -> Action -> Target -> Either Respon
responseContentTypeOrError accepts action target = serves contentTypesForRequest accepts responseContentTypeOrError accepts action target = serves contentTypesForRequest accepts
where where
contentTypesForRequest = case action of contentTypesForRequest = case action of
ActionRead -> [CTApplicationJSON, CTSingularJSON, CTTextCSV, CTOctetStream] ActionRead -> [CTApplicationJSON, CTSingularJSON, CTTextCSV] ++ rawContentTypes
ActionCreate -> [CTApplicationJSON, CTSingularJSON, CTTextCSV] ActionCreate -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
ActionUpdate -> [CTApplicationJSON, CTSingularJSON, CTTextCSV] ActionUpdate -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
ActionDelete -> [CTApplicationJSON, CTSingularJSON, CTTextCSV] ActionDelete -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
ActionInvoke _ -> [CTApplicationJSON, CTSingularJSON, CTTextCSV, CTOctetStream] ++ [CTOpenAPI | tpIsRootSpec target] ActionInvoke _ -> [CTApplicationJSON, CTSingularJSON, CTTextCSV] ++ rawContentTypes ++
[CTOpenAPI | tpIsRootSpec target]
ActionInspect -> [CTOpenAPI, CTApplicationJSON] ActionInspect -> [CTOpenAPI, CTApplicationJSON]
ActionInfo -> [CTTextCSV] ActionInfo -> [CTTextCSV]
ActionSingleUpsert -> [CTApplicationJSON, CTSingularJSON, CTTextCSV] ActionSingleUpsert -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
@@ -347,14 +348,18 @@ responseContentTypeOrError accepts action target = serves contentTypesForRequest
Nothing -> Left . errorResponseFor . ContentTypeError . map toMime $ cAccepts Nothing -> Left . errorResponseFor . ContentTypeError . map toMime $ cAccepts
Just ct -> Right ct Just ct -> Right ct
{-
| If raw(binary) output is requested, check that ContentType is one of the admitted rawContentTypes and that
| `?select=...` contains only one field other than `*`
-}
binaryField :: ContentType -> [FieldName] -> Either Response (Maybe FieldName) binaryField :: ContentType -> [FieldName] -> Either Response (Maybe FieldName)
binaryField CTOctetStream fldNames = binaryField ct fldNames
if length fldNames == 1 && fieldName /= Just "*" | ct `elem` rawContentTypes =
then Right fieldName let fieldName = headMay fldNames in
else Left . errorResponseFor $ BinaryFieldError if length fldNames == 1 && fieldName /= Just "*"
where then Right fieldName
fieldName = headMay fldNames else Left . errorResponseFor $ BinaryFieldError ct
binaryField _ _ = Right Nothing | otherwise = Right Nothing
splitKeyValue :: BS.ByteString -> (BS.ByteString, BS.ByteString) splitKeyValue :: BS.ByteString -> (BS.ByteString, BS.ByteString)
splitKeyValue kv = (k, BS.tail v) splitKeyValue kv = (k, BS.tail v)
@@ -385,6 +390,3 @@ contentRangeH lower upper total =
extractQueryResult :: Maybe ResultsWithCount -> ResultsWithCount extractQueryResult :: Maybe ResultsWithCount -> ResultsWithCount
extractQueryResult = fromMaybe (Nothing, 0, [], "") extractQueryResult = fromMaybe (Nothing, 0, [], "")
singularityError :: (Integral a) => a -> SimpleError
singularityError = SingularityError . toInteger
+8 -4
View File
@@ -12,6 +12,7 @@ module PostgREST.Error (
, SimpleError(..) , SimpleError(..)
, errorPayload , errorPayload
, checkIsFatal , checkIsFatal
, singularityError
) where ) where
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
@@ -190,7 +191,7 @@ checkIsFatal _ = Nothing
data SimpleError data SimpleError
= GucHeadersError = GucHeadersError
| BinaryFieldError | BinaryFieldError ContentType
| ConnectionLostError | ConnectionLostError
| PutSingletonError | PutSingletonError
| PutMatchingPkError | PutMatchingPkError
@@ -204,7 +205,7 @@ data SimpleError
instance PgrstError SimpleError where instance PgrstError SimpleError where
status GucHeadersError = HT.status500 status GucHeadersError = HT.status500
status BinaryFieldError = HT.status406 status (BinaryFieldError _) = HT.status406
status ConnectionLostError = HT.status503 status ConnectionLostError = HT.status503
status PutSingletonError = HT.status400 status PutSingletonError = HT.status400
status PutMatchingPkError = HT.status400 status PutMatchingPkError = HT.status400
@@ -222,8 +223,8 @@ instance PgrstError SimpleError where
instance JSON.ToJSON SimpleError where instance JSON.ToJSON SimpleError where
toJSON GucHeadersError = JSON.object [ toJSON GucHeadersError = JSON.object [
"message" .= ("response.headers guc must be a JSON array composed of objects with a single key and a string value" :: Text)] "message" .= ("response.headers guc must be a JSON array composed of objects with a single key and a string value" :: Text)]
toJSON BinaryFieldError = JSON.object [ toJSON (BinaryFieldError ct) = JSON.object [
"message" .= ((toS (toMime CTOctetStream) <> " requested but a single column was not selected") :: Text)] "message" .= ((toS (toMime ct) <> " requested but a single column was not selected") :: Text)]
toJSON ConnectionLostError = JSON.object [ toJSON ConnectionLostError = JSON.object [
"message" .= ("Database connection lost, retrying the connection." :: Text)] "message" .= ("Database connection lost, retrying the connection." :: Text)]
@@ -250,3 +251,6 @@ instance JSON.ToJSON SimpleError where
invalidTokenHeader :: Text -> Header invalidTokenHeader :: Text -> Header
invalidTokenHeader m = invalidTokenHeader m =
("WWW-Authenticate", "Bearer error=\"invalid_token\", " <> "error_description=" <> show m) ("WWW-Authenticate", "Bearer error=\"invalid_token\", " <> "error_description=" <> show m)
singularityError :: (Integral a) => a -> SimpleError
singularityError = SingularityError . toInteger
+31 -8
View File
@@ -6,11 +6,13 @@ Description : PostgREST common types and functions used by the rest of the modul
module PostgREST.Types where module PostgREST.Types where
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString as BS
import qualified Data.CaseInsensitive as CI import qualified Data.ByteString.Internal as BS (c2w)
import qualified Data.HashMap.Strict as M import qualified Data.ByteString.Lazy as BL
import qualified Data.Set as S import qualified Data.CaseInsensitive as CI
import qualified Data.HashMap.Strict as M
import qualified Data.Set as S
import qualified GHC.Show import qualified GHC.Show
import Network.HTTP.Types.Header (Header, hContentType) import Network.HTTP.Types.Header (Header, hContentType)
@@ -21,9 +23,10 @@ import PostgREST.RangeQuery (NonnegRange)
import Protolude import Protolude
-- | Enumeration of currently supported response content types -- | Enumeration of currently supported response content types
data ContentType = CTApplicationJSON | CTTextCSV | CTOpenAPI data ContentType = CTApplicationJSON | CTSingularJSON
| CTSingularJSON | CTOctetStream | CTTextCSV | CTTextPlain | CTTextHtml
| CTAny | CTOther ByteString deriving Eq | CTOpenAPI | CTOctetStream
| CTAny | CTOther ByteString deriving (Show, Eq)
-- | Convert from ContentType to a full HTTP Header -- | Convert from ContentType to a full HTTP Header
toHeader :: ContentType -> Header toHeader :: ContentType -> Header
@@ -33,12 +36,32 @@ toHeader ct = (hContentType, toMime ct <> "; charset=utf-8")
toMime :: ContentType -> ByteString toMime :: ContentType -> ByteString
toMime CTApplicationJSON = "application/json" toMime CTApplicationJSON = "application/json"
toMime CTTextCSV = "text/csv" toMime CTTextCSV = "text/csv"
toMime CTTextPlain = "text/plain"
toMime CTTextHtml = "text/html"
toMime CTOpenAPI = "application/openapi+json" toMime CTOpenAPI = "application/openapi+json"
toMime CTSingularJSON = "application/vnd.pgrst.object+json" toMime CTSingularJSON = "application/vnd.pgrst.object+json"
toMime CTOctetStream = "application/octet-stream" toMime CTOctetStream = "application/octet-stream"
toMime CTAny = "*/*" toMime CTAny = "*/*"
toMime (CTOther ct) = ct toMime (CTOther ct) = ct
-- | Convert from ByteString to ContentType. Warning: discards MIME parameters
decodeContentType :: BS.ByteString -> ContentType
decodeContentType ct = case BS.takeWhile (/= BS.c2w ';') ct of
"application/json" -> CTApplicationJSON
"text/csv" -> CTTextCSV
"text/plain" -> CTTextPlain
"text/html" -> CTTextHtml
"application/openapi+json" -> CTOpenAPI
"application/vnd.pgrst.object+json" -> CTSingularJSON
"application/vnd.pgrst.object" -> CTSingularJSON
"application/octet-stream" -> CTOctetStream
"*/*" -> CTAny
ct' -> CTOther ct'
-- | ContentTypes that can get a raw/unwrapped response
rawContentTypes :: [ContentType]
rawContentTypes = [CTOctetStream, CTTextPlain, CTTextHtml]
data PreferResolution = MergeDuplicates | IgnoreDuplicates deriving Eq data PreferResolution = MergeDuplicates | IgnoreDuplicates deriving Eq
instance Show PreferResolution where instance Show PreferResolution where
show MergeDuplicates = "resolution=merge-duplicates" show MergeDuplicates = "resolution=merge-duplicates"
+30 -49
View File
@@ -895,58 +895,39 @@ spec = do
[json|[{":arr->ow::cast":" arrow-1 ","(inside,parens)":" parens-1 ","a.dotted.column":" dotted-1 "," col w space ":" space-1"}]|] [json|[{":arr->ow::cast":" arrow-1 ","(inside,parens)":" parens-1 ","a.dotted.column":" dotted-1 "," col w space ":" space-1"}]|]
{ matchHeaders = [matchContentTypeJson] } { matchHeaders = [matchContentTypeJson] }
describe "binary output" $ do context "binary output" $ do
context "on GET" $ do it "can query if a single column is selected" $
it "can query if a single column is selected" $ request methodGet "/images_base64?select=img&name=eq.A.png" (acceptHdrs "application/octet-stream") ""
request methodGet "/images_base64?select=img&name=eq.A.png" (acceptHdrs "application/octet-stream") "" `shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCC"
`shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCC" { matchStatus = 200
{ matchStatus = 200 , matchHeaders = ["Content-Type" <:> "application/octet-stream; charset=utf-8"]
, matchHeaders = ["Content-Type" <:> "application/octet-stream; charset=utf-8"] }
}
it "fails if a single column is not selected" $ do it "can get raw output with Accept: text/plain" $
request methodGet "/images?select=img,name&name=eq.A.png" (acceptHdrs "application/octet-stream") "" request methodGet "/projects?select=name&id=eq.1" (acceptHdrs "text/plain") ""
`shouldRespondWith` `shouldRespondWith` "Windows 7"
[json| {"message":"application/octet-stream requested but a single column was not selected"} |] { matchStatus = 200
{ matchStatus = 406 , matchHeaders = ["Content-Type" <:> "text/plain; charset=utf-8"]
, matchHeaders = [matchContentTypeJson] }
}
request methodGet "/images?select=*&name=eq.A.png" (acceptHdrs "application/octet-stream") ""
`shouldRespondWith` 406
request methodGet "/images?name=eq.A.png" (acceptHdrs "application/octet-stream") ""
`shouldRespondWith` 406
it "concatenates results if more than one row is returned" $ it "fails if a single column is not selected" $ do
request methodGet "/images_base64?select=img&name=in.(A.png,B.png)" (acceptHdrs "application/octet-stream") "" request methodGet "/images?select=img,name&name=eq.A.png" (acceptHdrs "application/octet-stream") ""
`shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCCiVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEX///8AAP94wDzzAAAAL0lEQVQIW2NgwAb+HwARH0DEDyDxwAZEyGAhLODqHmBRzAcn5GAS///A1IF14AAA5/Adbiiz/0gAAAAASUVORK5CYII=" `shouldRespondWith`
{ matchStatus = 200 [json| {"message":"application/octet-stream requested but a single column was not selected"} |]
, matchHeaders = ["Content-Type" <:> "application/octet-stream; charset=utf-8"] { matchStatus = 406
} , matchHeaders = [matchContentTypeJson]
}
request methodGet "/images?select=*&name=eq.A.png" (acceptHdrs "application/octet-stream") ""
`shouldRespondWith` 406
request methodGet "/images?name=eq.A.png" (acceptHdrs "application/octet-stream") ""
`shouldRespondWith` 406
context "on RPC" $ do it "concatenates results if more than one row is returned" $
context "Proc that returns scalar" $ request methodGet "/images_base64?select=img&name=in.(A.png,B.png)" (acceptHdrs "application/octet-stream") ""
it "can query without selecting column" $ `shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCCiVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEX///8AAP94wDzzAAAAL0lEQVQIW2NgwAb+HwARH0DEDyDxwAZEyGAhLODqHmBRzAcn5GAS///A1IF14AAA5/Adbiiz/0gAAAAASUVORK5CYII="
request methodPost "/rpc/ret_base64_bin" (acceptHdrs "application/octet-stream") "" { matchStatus = 200
`shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCC" , matchHeaders = ["Content-Type" <:> "application/octet-stream; charset=utf-8"]
{ matchStatus = 200 }
, matchHeaders = ["Content-Type" <:> "application/octet-stream; charset=utf-8"]
}
context "Proc that returns rows" $ do
it "can query if a single column is selected" $
request methodPost "/rpc/ret_rows_with_base64_bin?select=img" (acceptHdrs "application/octet-stream") ""
`shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCCiVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEX///8AAP94wDzzAAAAL0lEQVQIW2NgwAb+HwARH0DEDyDxwAZEyGAhLODqHmBRzAcn5GAS///A1IF14AAA5/Adbiiz/0gAAAAASUVORK5CYII="
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/octet-stream; charset=utf-8"]
}
it "fails if a single column is not selected" $
request methodPost "/rpc/ret_rows_with_base64_bin" (acceptHdrs "application/octet-stream") ""
`shouldRespondWith`
[json| {"message":"application/octet-stream requested but a single column was not selected"} |]
{ matchStatus = 406
, matchHeaders = [matchContentTypeJson]
}
describe "HTTP request env vars" $ do describe "HTTP request env vars" $ do
it "custom header is set" $ it "custom header is set" $
+49
View File
@@ -469,6 +469,55 @@ spec actualPgVersion =
{"id":4,"name":"OSX"}] {"id":4,"name":"OSX"}]
|] { matchHeaders = [matchContentTypeJson] } |] { matchHeaders = [matchContentTypeJson] }
context "binary output" $ do
context "Proc that returns scalar" $ do
it "can query without selecting column" $
request methodPost "/rpc/ret_base64_bin" (acceptHdrs "application/octet-stream") ""
`shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCC"
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/octet-stream; charset=utf-8"]
}
it "can get raw output with Accept: text/html" $
request methodGet "/rpc/welcome.html" (acceptHdrs "text/html") ""
`shouldRespondWith`
[str|
|<html>
| <head>
| <title>PostgREST</title>
| </head>
| <body>
| <h1>Welcome to PostgREST</h1>
| </body>
|</html>
|]
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "text/html; charset=utf-8"]
}
it "can get raw output with Accept: text/plain" $
request methodGet "/rpc/welcome" (acceptHdrs "text/plain") ""
`shouldRespondWith` "Welcome to PostgREST"
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "text/plain; charset=utf-8"]
}
context "Proc that returns rows" $ do
it "can query if a single column is selected" $
request methodPost "/rpc/ret_rows_with_base64_bin?select=img" (acceptHdrs "application/octet-stream") ""
`shouldRespondWith` "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCCiVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEX///8AAP94wDzzAAAAL0lEQVQIW2NgwAb+HwARH0DEDyDxwAZEyGAhLODqHmBRzAcn5GAS///A1IF14AAA5/Adbiiz/0gAAAAASUVORK5CYII="
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/octet-stream; charset=utf-8"]
}
it "fails if a single column is not selected" $
request methodPost "/rpc/ret_rows_with_base64_bin" (acceptHdrs "application/octet-stream") ""
`shouldRespondWith`
[json| {"message":"application/octet-stream requested but a single column was not selected"} |]
{ matchStatus = 406
, matchHeaders = [matchContentTypeJson]
}
context "only for GET rpc" $ do context "only for GET rpc" $ do
it "should fail on mutating procs" $ do it "should fail on mutating procs" $ do
get "/rpc/callcounter" `shouldRespondWith` 500 get "/rpc/callcounter" `shouldRespondWith` 500
+17
View File
@@ -1713,3 +1713,20 @@ case current_setting('request.header.accept', true)
end case; end case;
end end
$_$ language plpgsql; $_$ language plpgsql;
create or replace function welcome() returns text as $$
select 'Welcome to PostgREST'::text;
$$ language sql;
create or replace function "welcome.html"() returns text as $_$
select $$
<html>
<head>
<title>PostgREST</title>
</head>
<body>
<h1>Welcome to PostgREST</h1>
</body>
</html>
$$::text;
$_$ language sql;