haskell-servant / haskell-servant/servant
Allow customizing the response code at the term level (at runtime) without sacrificing content negotiation
- Dominant language
- Haskell
- Stars
- 2k
- Forks
- 427
- Avg merge
- 2d 23h
- Merged PRs (30d)
- 5
Description
*I was encouraged to open this issue based on a [brief Twitter conversation](https://twitter.com/alpmestan/status/857015090394083328) with @alpmestan.*
Currently, servant is super awesome as long as nothing goes wrong. I can write a high-level, declarative specification of what each of my API endpoints responds with, and I can keep all the rendering code separate from my logic. For example, imagine I have a simple API that looks like this:
```haskell
newtype Quotient = Quotient Double
type API = "divide" :> Capture "x" Double :> Capture "y" Double :> Get '[JSON, HTML] Quotient
divide :: Double -> Double -> Handler Quotient
divide x y = return $ Quotient (x / y)
```
If the client requests `/divide/6/3` in JSON, then the result might look like this:
```json
{ "result": 2.0 }
```
If the client requests HTML, then the result might look like this:
```html
Divide
The result is: 2.0.
```
This is great! But what if the user is naughty, and they try to request `/divide/1/0`? Obviously, I don’t want my server to blow up, so I tweak my handler to handle this case:
```haskell
data Quotient
= QuotientResult Double
| DivisionByZero
divide :: Double -> Double -> Handler Quotient
divide _ 0 = return DivisionByZero
divide x y = return $ QuotientResult (x / y)
```
Now I can add a case to my `ToJSON` and `ToHTML` instances to produce different errors upon failure, depending on the requested content type:
```json
{ "error": "DivisionByZero" }
```
```html
Divide
Error: you cannot divide by zero!
```
However, I have a problem with this: my server is still responding with 200 OK, even though the response is obviously an error. It seems like my only option here is to respond with `ServantErr`, which makes sense, given that this is an error case, and I want the short-circuiting behavior of `ExceptT`. Sadly, if I produce `ServantErr`, I lose my nicely-formatted errors based on what content type the client requested.
In practice, I usually use a custom monad transformer stack for my handlers, and I use `Nat` to convert it to `Handler`. This doesn’t actually help at all, though, since I don’t have any access to the content type information at that point, so I can’t make the decision there.
There are probably a few ways to handle this. One way would be to add a different combinator, `GetWithStatus`, that allows the handler to return `Handler (StatusCode, a)` instead of `Handler a`. The downside to this, of course, is that it is remarkably non-typesafe, and it means you don’t get short-circuiting behavior of `ExceptT`.
A better solution would probably be to have a custom error type for an entire API, rather than `ServantErr`, which would be able to take advantage of content types to control how they are presented in the response. Here’s a completely imaginary interface for something like that:
```haskell
data CalculatorError = Overflow | DivisionByZero
instance ToStatusCode CalculatorError where
toStatusCode _ = 422
instance ToJSON CalculatorError where { ... }
instance ToHTML CalculatorError where { ... }
type API = UsingError CalculatorError :>
( "multiply" :> Capture "x" Double :> Capture "y" Double :> GetWithError '[JSON, HTML] Product
, "divide" :> Capture "x" Double :> Capture "y" Double :> GetWithError '[JSON, HTML] Quotient
)
multiply :: Double -> Double -> ExceptT CalculatorError Product
divide :: Double -> Double -> ExceptT CalculatorError Quotient
```
However, I’m not sure how feasible that is.
This seems related to #353 and #685, though this seems somewhat different, since it’s about application-specific errors rather than errors produced by servant. It’s probably the same issue as #296, though I’m not sure if things have changed much since then.
Contributor guide
Assessment
This issue has not been assessed yet.