2

I'm writing a webhook endpoint (receiving end) and don't really have control over the incoming Accept header in the request. Here's what it is:

Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2

I've tried Post '[JSON, HTML, PlainText] Text but it results in a 406 status code.

IIUC, Servant is unable to parse this as a valid Accept header due to the * (which should probably be */*) and the q=.2 (which should probably be q=0.2

How do I deal with this? The realistic situation is that I don't care about the Accept header, and the webhook sender doesn't really care about the response body (only the response code matters)

I found Network.HTTP.Media.Accept.Accept which has parseAccept :: ByteString -> Maybe a, which I tried using like this...

data IrrelevantAcceptHeader = IrrelevantAcceptHeader deriving (Show)

instance Network.HTTP.Media.Accept.Accept IrrelevantAcceptHeader where
  parseAccept _ = Just IrrelevantAcceptHeader
  matches _ _ = True
  moreSpecificThan _ _ = False
  hasExtensionParameters _ = True

instance Servant.Accept IrrelevantAcceptHeader where
  contentType _ = fromString "text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2"

instance MimeRender IrrelevantAcceptHeader Text where
  mimeRender _ txt = toS txt

-- and here's how it's used:

data Routes route = Routes
  { rWebhook 
    :: route 
    :- "webhook" 
    :> Header' '[Required, Strict] "X-Api-Secret" Text 
    :> ReqBody '[JSON] Aeson.Value 
    :> Post '[IrrelevantAcceptHeader] Text
  } deriving (Generic)

...but all this jugglery doesn't really work!

PS: This might be related to Haskell Servant (client): UnsupportedContentType error due to weird Accept header

Saurabh Nanda
  • 6,373
  • 5
  • 31
  • 60
  • How are you using `IrrelevantAcceptHeader`? Just defining the type (and its instances) is almost certainly not enough (barring some Template Haskell tomfoolery). – Daniel Wagner Jan 24 '23 at 22:03
  • @DanielWagner updated the question to specify how `IrrelevantAcceptHeader` is used, i.e. `Post '[IrrelevantAcceptHeader] Text` – Saurabh Nanda Jan 25 '23 at 01:59

1 Answers1

3

You could consider writing a Middleware to fix up the broken Accept header before it's passed to servant. This would affect all routes, but that's probably what you want anyway.

It would look something like:

import Network.Wai
import Network.HTTP.Types.Header

fixAccept :: Middleware
fixAccept app req
  = app (req { requestHeaders = map fixAcceptHeader (requestHeaders req) })
  where fixAcceptHeader (key, value) 
          | key == hAccept = (hAccept, value)  -- do something to "value" here
        fixAcceptHeader other = other

and when you run your Servant server, just wrap it in the middleware:

main :: IO ()
main = run 8080 (fixAccept app1)

If you want to check in your Middleware whether or not a header fix is necessary, note that Servant uses matchAccept from Network.HTTP.Media in the http-media package which in turn uses parseQuality to do the matching. You can check in the middleware if parseQuality succeeds or fails:

λ> :set -XOverloadedStrings
λ> import Data.ByteString
λ> import Network.HTTP.Media
λ> parseQuality "Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2" :: Maybe [Quality ByteString]
Nothing
λ> parseQuality "Accept: text/html, image/gif, image/jpeg, *; q=0.2, */*; q=0.2" :: Maybe [Quality ByteString]
Just [Accept: text/html;q=1,image/gif;q=1,image/jpeg;q=1,*;q=0.2,*/*;q=0.2]

As above, it appears to be the invalid quality numbers specifically that are causing problems.

This seems to be a known issue that, unfortunately, the developers are refusing to fix. Fortunately, http-media is open source with a permissive license, so you are free to patch it yourself for your own use or for redistribution.

K. A. Buhr
  • 45,621
  • 3
  • 45
  • 71
  • I was afraid to get this answer. This makes `servant` to use very problematic in real-life/pragmatic scenarios because of its insistence on implementing content-type negotiation so strictly. Would you, by any chance, know which function inside `servant` parses the accept header? Is there any way to know (inside the middleware) if the Accept header parsing has not worked? Only then apply a fix? – Saurabh Nanda Jan 29 '23 at 06:54
  • See the update at the end. – K. A. Buhr Jan 29 '23 at 18:55