Skip to content

Commit 9131f4f

Browse files
authored
Fix multiple headers with the same name (#1666)
1 parent 659a8c6 commit 9131f4f

File tree

4 files changed

+30
-12
lines changed

4 files changed

+30
-12
lines changed

changelog.d/1665

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
synopsis: Fix the handling of multiple headers with the same name.
2+
prs: #1666
3+
4+
description: {
5+
6+
servant-client no longer concatenates the values of response headers with the same name.
7+
This fixes an issue with parsing multiple `Set-Cookie` headers.
8+
9+
}

servant-client/test/Servant/ClientTestUtils.hs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ carol :: Person
107107
carol = Person "Carol" 17
108108

109109
type TestHeaders = '[Header "X-Example1" Int, Header "X-Example2" String]
110+
type TestSetCookieHeaders = '[Header "Set-Cookie" String, Header "Set-Cookie" String]
110111

111112
data RecordRoutes mode = RecordRoutes
112113
{ version :: mode :- "version" :> Get '[JSON] Int
@@ -151,6 +152,7 @@ type Api =
151152
Get '[JSON] (String, Maybe Int, Bool, [(String, [Rational])])
152153
:<|> "headers" :> Get '[JSON] (Headers TestHeaders Bool)
153154
:<|> "uverb-headers" :> UVerb 'GET '[JSON] '[ WithStatus 200 (Headers TestHeaders Bool), WithStatus 204 String ]
155+
:<|> "set-cookie-headers" :> Get '[JSON] (Headers TestSetCookieHeaders Bool)
154156
:<|> "deleteContentType" :> DeleteNoContent
155157
:<|> "redirectWithCookie" :> Raw
156158
:<|> "empty" :> EmptyAPI
@@ -184,6 +186,7 @@ getMultiple :: String -> Maybe Int -> Bool -> [(String, [Rational])]
184186
-> ClientM (String, Maybe Int, Bool, [(String, [Rational])])
185187
getRespHeaders :: ClientM (Headers TestHeaders Bool)
186188
getUVerbRespHeaders :: ClientM (Union '[ WithStatus 200 (Headers TestHeaders Bool), WithStatus 204 String ])
189+
getSetCookieHeaders :: ClientM (Headers TestSetCookieHeaders Bool)
187190
getDeleteContentType :: ClientM NoContent
188191
getRedirectWithCookie :: HTTP.Method -> ClientM Response
189192
uverbGetSuccessOrRedirect :: Bool
@@ -210,6 +213,7 @@ getRoot
210213
:<|> getMultiple
211214
:<|> getRespHeaders
212215
:<|> getUVerbRespHeaders
216+
:<|> getSetCookieHeaders
213217
:<|> getDeleteContentType
214218
:<|> getRedirectWithCookie
215219
:<|> EmptyClient
@@ -247,6 +251,7 @@ server = serve api (
247251
:<|> (\ a b c d -> return (a, b, c, d))
248252
:<|> (return $ addHeader 1729 $ addHeader "eg2" True)
249253
:<|> (pure . Z . I . WithStatus $ addHeader 1729 $ addHeader "eg2" True)
254+
:<|> (return $ addHeader "cookie1" $ addHeader "cookie2" True)
250255
:<|> return NoContent
251256
:<|> (Tagged $ \ _request respond -> respond $ Wai.responseLBS HTTP.found302 [("Location", "testlocation"), ("Set-Cookie", "testcookie=test")] "")
252257
:<|> emptyServer

servant-client/test/Servant/SuccessSpec.hs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,12 @@ successSpec = beforeAll (startWaiApp server) $ afterAll endWaiApp $ do
150150
-> getHeaders val' `shouldBe` [("X-Example1", "1729"), ("X-Example2", "eg2")]
151151
Nothing -> assertFailure "unexpected alternative of union"
152152

153+
it "Returns multiple Set-Cookie headers appropriately" $ \(_, baseUrl) -> do
154+
res <- runClient getSetCookieHeaders baseUrl
155+
case res of
156+
Left e -> assertFailure $ show e
157+
Right val -> getHeaders val `shouldBe` [("Set-Cookie", "cookie1"), ("Set-Cookie", "cookie2")]
158+
153159
it "Stores Cookie in CookieJar after a redirect" $ \(_, baseUrl) -> do
154160
mgr <- C.newManager C.defaultManagerSettings
155161
cj <- atomically . newTVar $ C.createCookieJar []

servant/src/Servant/API/ResponseHeaders.hs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import Control.DeepSeq
3939
import Data.ByteString.Char8 as BS
4040
(ByteString, init, pack, unlines)
4141
import qualified Data.CaseInsensitive as CI
42+
import qualified Data.List as L
4243
import Data.Proxy
4344
import Data.Typeable
4445
(Typeable)
@@ -97,24 +98,21 @@ type family HeaderValMap (f :: * -> *) (xs :: [*]) where
9798

9899
class BuildHeadersTo hs where
99100
buildHeadersTo :: [HTTP.Header] -> HList hs
100-
-- ^ Note: if there are multiple occurrences of a header in the argument,
101-
-- the values are interspersed with commas before deserialization (see
102-
-- <http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 RFC2616 Sec 4.2>)
103101

104102
instance {-# OVERLAPPING #-} BuildHeadersTo '[] where
105103
buildHeadersTo _ = HNil
106104

105+
-- The current implementation does not manipulate HTTP header field lines in any way,
106+
-- like merging field lines with the same field name in a single line.
107107
instance {-# OVERLAPPABLE #-} ( FromHttpApiData v, BuildHeadersTo xs, KnownSymbol h )
108108
=> BuildHeadersTo (Header h v ': xs) where
109-
buildHeadersTo headers =
110-
let wantedHeader = CI.mk . pack $ symbolVal (Proxy :: Proxy h)
111-
matching = snd <$> filter (\(h, _) -> h == wantedHeader) headers
112-
in case matching of
113-
[] -> MissingHeader `HCons` buildHeadersTo headers
114-
xs -> case parseHeader (BS.init $ BS.unlines xs) of
115-
Left _err -> UndecodableHeader (BS.init $ BS.unlines xs)
116-
`HCons` buildHeadersTo headers
117-
Right h -> Header h `HCons` buildHeadersTo headers
109+
buildHeadersTo headers = case L.find wantedHeader headers of
110+
Nothing -> MissingHeader `HCons` buildHeadersTo headers
111+
Just header@(_, val) -> case parseHeader val of
112+
Left _err -> UndecodableHeader val `HCons` buildHeadersTo (L.delete header headers)
113+
Right h -> Header h `HCons` buildHeadersTo (L.delete header headers)
114+
where wantedHeader (h, _) = h == wantedHeaderName
115+
wantedHeaderName = CI.mk . pack $ symbolVal (Proxy :: Proxy h)
118116

119117
-- * Getting headers
120118

0 commit comments

Comments
 (0)