Skip to content

Commit 83c6bcf

Browse files
committed
fix: content-type not set with custom domain and accept */*
BREAKING CHANGE Adds a `*/*` to available media type mapping. Now, the server responds with available media type when `Accept: */*`. Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
1 parent 35de13e commit 83c6bcf

File tree

7 files changed

+103
-49
lines changed

7 files changed

+103
-49
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. From versio
44

55
## Unreleased
66

7+
## Fixed
8+
9+
- Fix `Content-Type` not set with custom domain type and `Accept: */*` by @taimoorzaeem in #3391
10+
711
## [14.1] - 2025-11-05
812

913
## Fixed

src/PostgREST/ApiRequest.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ userApiRequest conf prefs req reqBody = do
9898
, iMethod = method
9999
, iSchema = schema
100100
, iNegotiatedByProfile = negotiatedByProfile
101-
, iAcceptMediaType = maybe [MTAny] (map MediaType.decodeMediaType . parseHttpAccept) $ lookupHeader "accept"
101+
, iAcceptMediaType = maybe [MTApplicationJSON] (map MediaType.decodeMediaType . parseHttpAccept) $ lookupHeader "accept"
102102
, iContentMediaType = contentMediaType
103103
}
104104
where

src/PostgREST/Plan.hs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,9 +1140,10 @@ negotiateContent conf ApiRequest{iAction=act, iPreferences=Preferences{preferRep
11401140
x -> lookupHandler x
11411141
mtPlanToNothing x = if configDbPlanEnabled conf then x else Nothing -- don't find anything if the plan media type is not allowed
11421142
lookupHandler mt =
1143-
when' defaultSelect (HM.lookup (RelId identifier, MTAny) produces) <|> -- lookup for identifier and `*/*`
1144-
when' defaultSelect (HM.lookup (RelId identifier, mt) produces) <|> -- lookup for identifier and a particular media type
1145-
HM.lookup (RelAnyElement, mt) produces -- lookup for anyelement and a particular media type
1143+
when' defaultSelect (HM.lookup (RelId identifier, mt) produces) <|> -- lookup for identifier and a particular media type
1144+
HM.lookup (RelAnyElement , mt) produces <|> -- lookup for anyelement and a particular media type
1145+
when' defaultSelect (HM.lookup (RelId identifier, MTAny) produces) -- lookup for identifier and */* media type
1146+
11461147
when' :: Bool -> Maybe a -> Maybe a
11471148
when' True (Just a) = Just a
11481149
when' _ _ = Nothing

src/PostgREST/SchemaCache.hs

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,17 +1078,45 @@ mediaHandlers =
10781078
proc.pronamespace = ANY($$1::regnamespace[]) and NOT proretset
10791079
and prokind = 'f'|]
10801080

1081+
-- We take the union of two maps:
1082+
-- First map builds the mapping of domain types -> resolved media type
1083+
-- Second map creates the same mapping, but with "*/*" key
1084+
-- Example:
1085+
-- +------------------------------------------------+
1086+
-- | domain type | resolved media type |
1087+
-- +------------------------------------------------+
1088+
-- | text/plain | text/plain |
1089+
-- +------------------------------------------------+
1090+
-- | */* | text/plain |
1091+
-- +------------------------------------------------+
1092+
-- This allows us to resolve to "text/plain" on both
1093+
-- "Accept: text/plain" and
1094+
-- "Accept: */*"
1095+
--
1096+
-- TODO: This basically doubles the size of the map, so we should do some
1097+
-- production scale memory tests to make sure with aren't hitting OOM errors
10811098
decodeMediaHandlers :: HD.Result MediaHandlerMap
1082-
decodeMediaHandlers =
1083-
HM.fromList . fmap (\(x, y, z, w) ->
1084-
let rel = if isAnyElement y then RelAnyElement else RelId y
1085-
in ((rel, z), (CustomFunc x rel, w)) ) <$> HD.rowList caggRow
1086-
where
1087-
caggRow = (,,,)
1088-
<$> (QualifiedIdentifier <$> column HD.text <*> column HD.text)
1089-
<*> (QualifiedIdentifier <$> column HD.text <*> column HD.text)
1090-
<*> (MediaType.decodeMediaType . encodeUtf8 <$> column HD.text)
1091-
<*> (MediaType.decodeMediaType . encodeUtf8 <$> column HD.text)
1099+
decodeMediaHandlers = mapFromList <$> HD.rowList caggRow
1100+
where
1101+
mapFromList aggRowList =
1102+
HM.union
1103+
(HM.fromList . fmap (aggRowToList False) $ aggRowList) -- type -> type map
1104+
(HM.fromList . fmap (aggRowToList True) $ aggRowList) -- */* -> type map
1105+
1106+
caggRow = (,,,)
1107+
<$> (QualifiedIdentifier <$> column HD.text <*> column HD.text)
1108+
<*> (QualifiedIdentifier <$> column HD.text <*> column HD.text)
1109+
<*> (MediaType.decodeMediaType . encodeUtf8 <$> column HD.text)
1110+
<*> (MediaType.decodeMediaType . encodeUtf8 <$> column HD.text)
1111+
1112+
aggRowToList withMTAny (x,y,z,w) = ((rel, mt), (CustomFunc x rel, w))
1113+
where
1114+
-- we don't add the */* mapping when rel is "anyelement" because we
1115+
-- already have (RelAnyElement, MTAny) in the initial media handler
1116+
-- map which maps to "application/json"
1117+
mt = if withMTAny && (rel /= RelAnyElement) then MediaType.MTAny else z
1118+
rel = if isAnyElement y then RelAnyElement else RelId y
1119+
10921120

10931121
timezones :: Bool -> SQL.Statement () TimezoneNames
10941122
timezones = SQL.Statement sql HE.noParams decodeTimezones

test/spec/Feature/Query/CustomMediaSpec.hs

Lines changed: 34 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module Feature.Query.CustomMediaSpec where
33
import Network.Wai (Application)
44

55
import Network.HTTP.Types
6-
import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus))
6+
import Network.Wai.Test (SResponse (simpleBody, simpleHeaders))
77
import Test.Hspec
88
import Test.Hspec.Wai
99
import Test.Hspec.Wai.JSON
@@ -28,14 +28,11 @@ spec = describe "custom media types" $ do
2828
simpleBody r `shouldBe` readFixtureFile "1.twkb"
2929
simpleHeaders r `shouldContain` [("Content-Type", "application/vnd.twkb")]
3030

31-
it "will fail if there's no aggregate defined for the table" $ do
32-
request methodGet "/lines" (acceptHdrs "text/plain") ""
33-
`shouldRespondWith`
34-
[json| {"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: text/plain"} |]
35-
{ matchStatus = 406
36-
, matchHeaders = [ matchContentTypeJson
37-
, "Content-Length" <:> "110" ]
38-
}
31+
it "will succeed if an aggregate exist with the correct media type" $ do
32+
r <- request methodGet "/lines" (acceptHdrs "text/plain") ""
33+
liftIO $ do
34+
simpleBody r `shouldBe` readFixtureFile "lines.twkb"
35+
simpleHeaders r `shouldContain` [("Content-Type", "application/vnd.twkb")]
3936

4037
it "can get raw xml output with Accept: text/xml if there's an aggregate defined" $ do
4138
request methodGet "/xmltest" (acceptHdrs "text/xml") ""
@@ -116,14 +113,22 @@ spec = describe "custom media types" $ do
116113
, matchHeaders = ["Content-Type" <:> "text/xml; charset=utf-8"]
117114
}
118115

119-
it "should fail with function returning text and Accept: text/xml" $ do
116+
it "should get the return type of function when accept is */*" $ do
117+
request methodGet "/rpc/javascript"
118+
[("Accept", "*/*")]
119+
""
120+
`shouldRespondWith`
121+
"This is Javascript."
122+
{ matchStatus = 200
123+
, matchHeaders = ["Content-Type" <:> "text/javascript"]
124+
}
125+
126+
it "should return the content-type as given in function return type" $ do
120127
request methodGet "/rpc/welcome" (acceptHdrs "text/xml") ""
121128
`shouldRespondWith`
122-
[json|
123-
{"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: text/xml"}
124-
|]
125-
{ matchStatus = 406
126-
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
129+
"Welcome to PostgREST"
130+
{ matchStatus = 200
131+
, matchHeaders = ["Content-Type" <:> "text/plain; charset=utf-8"]
127132
}
128133

129134
it "should not fail when the function doesn't return a row" $ do
@@ -160,12 +165,11 @@ spec = describe "custom media types" $ do
160165
simpleBody r `shouldBe` readFixtureFile "lines.twkb"
161166
simpleHeaders r `shouldContain` [("Content-Type", "application/vnd.twkb")]
162167

163-
it "fails if doesn't have an aggregate defined" $ do
164-
request methodGet "/rpc/get_lines"
165-
(acceptHdrs "application/octet-stream") ""
166-
`shouldRespondWith`
167-
[json| {"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: application/octet-stream"} |]
168-
{ matchStatus = 406 }
168+
it "return the content with the available aggregate and media type" $ do
169+
r <- request methodGet "/rpc/get_lines" (acceptHdrs "application/octet-stream") ""
170+
liftIO $ do
171+
simpleBody r `shouldBe` readFixtureFile "lines.twkb"
172+
simpleHeaders r `shouldContain` [("Content-Type", "application/vnd.twkb")]
169173

170174
-- TODO SOH (start of heading) is being added to results
171175
it "works if there's an anyelement aggregate defined" $ do
@@ -221,11 +225,15 @@ spec = describe "custom media types" $ do
221225
simpleHeaders r2 `shouldContain` [("Content-Length", "138")]
222226

223227
-- https://github.com/PostgREST/postgrest/issues/2170
224-
it "will match json in presence of text/plain" $ do
225-
r <- request methodGet "/projects?id=eq.1" (acceptHdrs "text/plain, application/json") ""
226-
liftIO $ do
227-
simpleStatus r `shouldBe` status200
228-
simpleHeaders r `shouldContain` [("Content-Type", "application/json; charset=utf-8")]
228+
it "TODO" $ do
229+
request methodGet "/projects?id=eq.1"
230+
(acceptHdrs "text/plain, application/json")
231+
""
232+
`shouldRespondWith`
233+
"id\tname\tclient_id\n1\tWindows 7\t1\n"
234+
{ matchStatus = 200
235+
, matchHeaders = [ "Content-Type" <:> "text/tab-separated-values" ]
236+
}
229237

230238
-- https://github.com/PostgREST/postgrest/issues/1102
231239
it "will match a custom text/tab-separated-values" $ do

test/spec/Feature/Query/RawOutputTypesSpec.hs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,20 @@ spec = describe "When raw-media-types config variable is missing or left empty"
2323
`shouldRespondWith` [json| [{"id":1}] |]
2424
{ matchHeaders= ["Content-Type" <:> "application/json; charset=utf-8"] }
2525

26-
it "responds json to a GET request to RPC with Firefox Accept headers" $
27-
request methodGet "/rpc/get_projects_below?id=3" firefoxAcceptHdrs ""
28-
`shouldRespondWith` [json|[{"id":1,"name":"Windows 7","client_id":1}, {"id":2,"name":"Windows 10","client_id":1}]|]
29-
{ matchHeaders= ["Content-Type" <:> "application/json; charset=utf-8"] }
30-
it "responds json to a GET request to RPC with Chrome Accept headers" $
31-
request methodGet "/rpc/get_projects_below?id=3" chromeAcceptHdrs ""
32-
`shouldRespondWith` [json|[{"id":1,"name":"Windows 7","client_id":1}, {"id":2,"name":"Windows 10","client_id":1}]|]
33-
{ matchHeaders= ["Content-Type" <:> "application/json; charset=utf-8"] }
26+
it "TODO" $
27+
request methodGet "/rpc/get_projects_below?id=3"
28+
firefoxAcceptHdrs ""
29+
`shouldRespondWith`
30+
"id\tname\tclient_id\n1\tWindows 7\t1\n2\tWindows 10\t1\n"
31+
{ matchStatus = 200
32+
, matchHeaders = ["Content-Type" <:> "text/tab-separated-values"]
33+
}
34+
35+
it "TODO" $
36+
request methodGet "/rpc/get_projects_below?id=3"
37+
chromeAcceptHdrs ""
38+
`shouldRespondWith`
39+
"id\tname\tclient_id\n1\tWindows 7\t1\n2\tWindows 10\t1\n"
40+
{ matchStatus = 200
41+
, matchHeaders = ["Content-Type" <:> "text/tab-separated-values"]
42+
}

test/spec/fixtures/schema.sql

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ SET search_path = test, pg_catalog;
107107
SET default_tablespace = '';
108108

109109
SET default_with_oids = false;
110-
110+
create domain "text/javascript" as text;
111111
create domain "text/plain" as text;
112112
create domain "text/html" as text;
113113
create domain "text/xml" as pg_catalog.xml;
@@ -1884,6 +1884,10 @@ create or replace function welcome() returns "text/plain" as $$
18841884
select 'Welcome to PostgREST'::"text/plain";
18851885
$$ language sql;
18861886

1887+
create or replace function javascript() returns "text/javascript" as $$
1888+
select 'This is Javascript.'::"text/javascript";
1889+
$$ language sql;
1890+
18871891
create or replace function welcome_twice() returns setof "text/plain" as $$
18881892
select 'Welcome to PostgREST'
18891893
union all

0 commit comments

Comments
 (0)