haskell-servant / haskell-servant/servant

Allow customizing the response code at the term level (at runtime) without sacrificing content negotiation

Open
#732 16 comments 12 reactions 0 assignees View on GitHub
ZuriHac17
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.