Search code examples
haskellservant

Sending Generic Content-Type in Servant


I am trying to relay some ByteString back to the client (browser). The client will not know the content-type of the document being requested so I am trying to send appropriate content-type response back to the client. The document could be an image or pdf or word document, etc.

For example, the client will request /document?id=55 and the server will respond with the appropriate content-type and the associated ByteString.

I followed the example here: and I created something for an image.

 data IMAGE

 instance Accept IMAGE where
     contentType _ = "image" M.// "jpeg"

 instance MimeRender IMAGE LBS.ByteString where
     mimeRender _ = id

The challenge is the client will not be sending the request with some specific Accept: header so there is no way for me to react with an appropriate Mime Type like it is done here. Plus the above will only work for images (assuming browsers will infer a png even I send back jpeg) but not for pdf, docx,etc.

I thought about a paramaterized type like MyDynamicContent String and I will pass in the content type at run-time but I am not sure how I will declare my API i.e., what will I use instead of '[JSON]. Not sure such thing is even possible as the examples are just a simple datatype.

So my question is, if I want to send some ByteString as a response and set the Content-Type header dynamically, what will be the best way to do it using servant

Update: I have opened an issue


Solution

  • It's possible, but a bit of a hack:

    {-# LANGUAGE DataKinds #-}
    {-# LANGUAGE FlexibleContexts #-}
    {-# LANGUAGE FlexibleInstances #-}
    {-# LANGUAGE MultiParamTypeClasses #-}
    {-# LANGUAGE OverloadedStrings #-}
    {-# LANGUAGE OverlappingInstances #-}
    module DynCT where
    
    import Control.Monad.Trans.Either
    import Data.ByteString.Lazy (ByteString)
    import Servant
    import Servant.API.ContentTypes
    import Network.Wai.Handler.Warp
    
    data WithCT = WithCT { header ::  ByteString, content :: ByteString }
    
    instance AllCTRender xs WithCT where
      handleAcceptH _ _ (WithCT h c) = Just (h, c)
    
    type API = Get '[] WithCT
    
    api :: Proxy API
    api = Proxy
    
    server :: Server API
    server = return $ WithCT { header = "example", content = "somecontent" }
    
    main :: IO ()
    main = run 8000 $ serve api server
    

    Testing it:

      % curl localhost:8000 -v
    ...
    < HTTP/1.1 200 OK
    ...
    < Content-Type: example
    <
    ...
    somecontent%
    

    The idea is just to override the normal behaviour by declaring an overlapping instance for AllCTRender. Note that you'll probably also have to do some extra leg work for servant-client, servant-docs etc. if you're also using those. Given that, you may want to open an issue in the repo about this for more complete support.