7

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

Ecognium
  • 2,046
  • 1
  • 19
  • 35
  • How does the server decide what content-type to respond with? (In particular, just to make sure, this cannot be statically determined?) – user2141650 Feb 26 '16 at 13:26
  • @user2141650: The `server` gets this info from the datastore (a document store service). It makes a call to the service and the service responds with the content-type and also the byte string. I know I can create an end point for each content-type (or mostly used ones at least) and send the content-type first and then ask the client to use the end point corresponding to the content-type. This is such a bad hack,moving a lot of this logic to the client, i feel it should be better handled by the server. – Ecognium Feb 26 '16 at 17:06

2 Answers2

4

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.

user2141650
  • 2,827
  • 1
  • 15
  • 23
  • Thank you so much. This indeed works. I am using `servant-docs` and I am getting type errors. You already knew this will be an issue. Not sure what I need to provide to satisfy docs for using `'[]`. The error I get is `Could not deduce (Servant.API.ContentTypes.IsNonEmpty '[]) arising from a use of ‘Doc.docs’`. I will open an issue and see if there is a clean way to solve the docs issue. – Ecognium Feb 26 '16 at 22:25
  • I made it work with the docs. Instead of using `'[]` I just used a generic custom mime type. The Accept instance's content-type is getting overriden by the `AllCTRender` so that works out well. This way I am able to use the docs and return a custom mime type. – Ecognium Feb 26 '16 at 22:51
  • It seems like this no longer works in servant 0.15, because it is always failing with status 406. – Christoph Schiessl Jan 26 '19 at 07:45
1

As of right now this hack works

data WithCT = WithCT {header :: BS.ByteString, content :: BS.ByteString}

instance AllCTRender '[IMAGE] WithCT where
  handleAcceptH _ _ (WithCT h c) = Just (fromStrict h, fromStrict c)

data IMAGE deriving (Typeable)

instance MimeRender IMAGE BS.ByteString where
  mimeRender _ content = fromStrict content

instance Accept IMAGE where
  contentType _ = ""

type ImageApi = Capture "image_id" ImageId :> Get '[IMAGE] WithCT