haskell-servant / haskell-servant/servant
`servant-client` ignores `Header'`'s mods, causing response headers to always be treated as `Optional` and `Lenient`
- Dominant language
- Haskell
- Stars
- 2k
- Forks
- 427
- Avg merge
- 2d 23h
- Merged PRs (30d)
- 5
Description
`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 `Required` header succeeds, but it should fail
- running the client against a server that responds with a `Strict` header that can't be parsed succeeds, but it should fail
- running the client against a server that *correctly* responds with a `Strict` header results in a `ResponseHeader` type, and the user still has to pattern match on `MissingHeader`, even though this branch should be unreachable in this case
- running the client against a server that *correctly* responds with a `Lenient` header results in a `ResponseHeader` type, and the user still has to pattern match on `UndecodableHeader`, 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`](https://github.com/intolerable/servant-header-verb) which might be interesting (but might not)
Full example below:
```haskell
#!/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 ()
```
Contributor guide
Assessment
This issue has not been assessed yet.