`servant-client` ignores `Header'`'s mods, causing response headers to always be treated as `Optional` and `Lenient`
HasClient 's instance for Verb m s c (Headers h a) ignores Header's modifiers, treating all response headers as Optional and Lenient. This causes several problems (imo):
- running the client against a server that doesn't respond with a
Requiredheader succeeds, but it should fail - running the client against a server that responds with a
Strictheader that can't be parsed succeeds, but it should fail - running the client against a server that correctly responds with a
Strictheader results in aResponseHeadertype, and the user still has to pattern match onMissingHeader, even though this branch should be unreachable in this case - running the client against a server that correctly responds with a
Lenientheader results in aResponseHeadertype, and the user still has to pattern match onUndecodableHeader, even though this branch should be unreachable in this case
(opinionated part follows:)
imo this is mostly fallout from hlistLookupHeader in HasResponseHeader having the wrong type. I think it should be returning one of headerType, Maybe headerType, Either headerType), or Maybe (Either headerType) (or an equivalent GADT), depending on the Header''s mods , but my brief attempt at changing that resulted in me painting myself into a corner with type family injectivity. if you think more explanation / code from this exploration would be useful, let me know. I also have a repository with a completely undocumented prototype that integrates response headers into Verb which might be interesting (but might not)
Full example below:
#!/usr/bin/env cabal
{- cabal:
default-language: GHC2021
default-extensions:
DataKinds
GADTs
LambdaCase
build-depends:
base,
aeson,
servant >=0.20 && < 0.21,
servant-client >=0.20 && < 0.21,
servant-client-core >=0.20 && < 0.21,
ghc-options: -Wall -Wextra -Wcompat -Werror
-}
module Main where
import Data.Proxy
import Servant.API
import Servant.Client
type BadHeaderAPI =
"test" :> Get '[PlainText] (Headers '[Header' '[Required, Strict] "X-Test-Header" Int] String)
badHeaderClient :: Client ClientM BadHeaderAPI
badHeaderClient = client (Proxy :: Proxy BadHeaderAPI)
getTestHeader :: ClientM Int
getTestHeader = do
badHeaderClient >>= \case
Headers _r hs -> case hs of
xTestHeader `HCons` HNil ->
case xTestHeader of
Header x -> pure x
MissingHeader ->
error "huh? this should be required, and `badHeaderClient` should immediately fail!"
UndecodableHeader _ ->
error "huh? parsing this header should be strict, and `badHeaderClient` should immediately fail!"
main :: IO ()
main = pure ()
@intolerable Hi, thanks for reporting this. I'd be really grateful if you could submit a PR to fix this, if you have the time. :)