haskell-servant / haskell-servant/servant

`servant-client` ignores `Header'`'s mods, causing response headers to always be treated as `Optional` and `Lenient`

Open
#1,822 1 comment 0 reactions 0 assignees View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.