Choosing Response Content-Type Dynamically in Servant Based on Result

Hello everyone,

I have a Servant endpoint defined as:

type API = "foo" :> ReqBody '[JSON] Foo :> Post '[JSON, PlainText] (Either Bar Baz)

I want the server to choose the response content type dynamically based on the result of the request:

  • If the response is Bar, I want to return it as JSON.
  • If the response is Baz, I want to return it as PlainText.

How can I make Servant select between JSON and PlainText at runtime depending on whether the result is Bar or Baz?

Thanks in advance for any guidance!

4 Likes

I couldn’t find a direct answer to the original question, but I came up with an alternative idea.
The main issue was that even if we specify multiple response content types like '[JSON, PlainText], the server developer cannot freely choose which one to return — Servant decides based solely on the client’s Accept header.

While reading the WAI documentation, I found that it’s possible to modify request headers using middleware. I’m currently working on implementing such a middleware, and I’ve run into a new problem that I’m trying to solve — but if it works, I’ll share the results here.

If this approach succeeds, it should also be possible to rewrite not only the Accept header but even the request path, which means we might be able to dynamically route requests to different endpoints.

I’ve previously hacked around this by (ab)using throwError and ServerError. Unfortunately this means that your API type won’t reflect what happens in reality, but I haven’t found a better way of dealing with this.

1 Like

@nattybear Indeed, it’s important that the Accept header is honoured according to the behaviours specified in the RFCs. Otherwise the client getting the response will get (understandably) confused by the shape of the response payload. May I ask what is your use-case?

In my case, it’s a situation where the user is supposed to receive data encoded in base64, so the request should be made with application/x-www-form-urlencoded. However, the user is sending the request with application/json instead, which is causing me a lot of trouble.

Hmm, at this stage you have a multitude of hacks available to you, although nothing beats having the client fixed (which I understand is not always possible or doable in a timely manner).

My advice would be for you to create your own ‘Accept’ type, with custom MimeRender and MimeUnrender instances, so that ‘[CursedJSON] accepts application/json and returns application/x-www-form-urlencoded. How does that sound?

1 Like

I actually had a similar idea at first. However, it seems that in Servant, each content type must have a one-to-one correspondence with its type. The following code, for example, ends up failing to compile because the first argument of contentType is defined as a phantom type:

data CursedJSON = JSON | Base64

instance Accept CursedJSON where
  contentType JSON   = "application" // "json"
  contentType Base64 = "application" // "x-www-form-urlencoded"

And how about using the contentTypes (notice the plural) method of Accept? You’d just have an empty CursedJSON data type instead of a sum type. :slight_smile:

Even if you implement it like this, I’m still not sure how the server programmer could choose the content type based on the return type they actually want. In the end, you can only hope that the client sends the correct Accept header—though, of course, I could override that with middleware!

data CursedJSON

instance Accept CursedJSON where
  contentTypes _ = fromList [ "application" // "json"
                            , "application" // "x-www-form-urlencoded" ]

Manipulating headers in the middleware worked fine, but even when I changed the Content-Type there, Servant did not render the response the way I expected.

Instead, I modified the path in the middleware to split the endpoint, and thanks to that, I was able to handle responses properly without having a single endpoint serve multiple content types. Thank you.

Thanks! I’d be interested to see the final code for this middleware. :slight_smile:

1 Like

I’d prefer not to share the full code publicly, but here’s roughly how I wrote the middleware.
The makeAction function is the same as the one I shared earlier, with only a few variable names changed.

middle :: Middleware
middle app req respond = do
  body   <- strictRequestBody req
  action <- makeAction body
  let req' = setRequestBodyChunks action req
  case checkSomething body of
    Nothing  -> app req' respond
    Just foo -> case foo of
      Foo -> do
        let newReq = req' { rawPathInfo = "/foo"
                          , pathInfo    = ["foo"] }
        app newReq respond
      _ -> app req' respond

Since the middleware uses strictRequestBody to read the entire request body and then restore it, the overall response time does get slower when requests pass through it.

When updating the Request record, I modified both rawPathInfo and pathInfo, but I haven’t checked whether changing just one of them would have the same effect.

1 Like

Since the middleware uses strictRequestBody to read the entire request body and then restore it, the overall response time does get slower when requests pass through it.

If you don’t need to read the entire request body, you could interleave them. It would be a bit more complex, but not by a lot. Basically, you need an IO action that returns the partial body, and then when it runs out reverts to the original getRequestBodyChunk

1 Like