Open lexi-lambda opened 7 years ago
I've thought about this a decent amount, though haven't really written much code for it. Here are two main options:
My opinion is that we should still have a single concrete type (like ServantErr
) for errors, rather than try to allow the user to pick one and then somewhere (morally) existentialize it, since we already have Nat
, which would allow picking different error types (as long as they're consistent throughout the server).
An obvious candidate is something like:
data NewServantErr = NewServantErr
{ acceptHeaderMapping :: Map AcceptHeader OldServantErr
, defaultErr :: OldServantErr
}
We'd of course provide convenience functions around that. Combinators could use this same format, so it'd be related to #353 and #685 insofar as we probably want something that works in both cases.
This approach is simple, easy to implement, and isn't as large a change for end-users as the next one. The downside is that:
Nat
combined with their own datatypes, replicating functionality that fits in quite naturally with the more ambitious approach.The other option is to have information about the error type in the API type. So e.g. instead of:
type API = Get '[JSON] SuccessResponse
We'd have
type API = Get '[JSON, HTML] ErrorResponse SuccessResponse
Where ErrorResponse
, like the SuccessResponse
, is allowed to vary by endpoint. ErrorResponse
is required to have the same instances (ToJSON
, ToMarkup
) as SuccessResponse
; because content-type negotiation happens before the handlers are run, we know that this is sufficient. Additionally, we would probably have:
class IsError a where
type PossibleStatusCodes :: [Nat]
statusCodeOf :: a -> StatusCode
Which ErrorResponse
would have to be an instance of.
Alternatively, instead of adding a type to the API type, we could start expecting the type of the handler response itself to have an instance of an IsError
-like class, but I'm not sure I like that.
Even though servant
would change quite a bit, it might be possible to make this nearly backwards-compatible with type aliases, which is reassuring.
I also don't think this one is too hard to implement, actually.
Note that I've somewhere advocated adding a lot more info to the API type. This is a sort of toned-down version of that.
In this approach, I'd say #353 and #685 are pretty much unrelated.
@lexi-lambda the Using Calculation
error is very similar to the second option above (sorry, should have mentioned that). I think in theory if we really wanted to we could have both forms (one being a type function for the other).
I like the ambitious approach. If we change Verb
but have Get
, Post
etc as what they are that would be quite invisible change!
EDIT: we'll need to be able to create ErrorResponse
from ServantErr
.
I like the "ambitious" approach as well, it feels like it's a better direction to follow. The trickiest part I think would be to come up with the simplest API that would expose this new mechanism to users. I'm not saying we should aim for a perfect API right from the start, but rather "let's put some thoughts into it now and refine over time as we get feedback".
Could/Should we leave this a project for ZuriHac'17? (we could create a label, and tag potential tasks)?
That could be a nice task indeed. If someone wants to tackle this (or even just get started) before that would be very welcome by everyone though I think =)
I also like the ambitious approach.
I agree with all the feedback here; the “ambitious” approach looks great. I’d consider working on implementing some of it myself, but I am not sure that I know nearly enough about how servant works to help much.
I was talking to @alpmestan about this through email and he suggested I post about it here.
I've wanted something like this for a while now, and I am currently writing some code to solve a similar problem to this. Here's a work-in-progress:
https://github.com/cdepillabout/servant-checked-exceptions
I currently haven't written very much documentation, but you can get an idea of how it works by looking at this example:
Also possibly the Envelope
type:
My main goals with this were the following:
servant-docs
. I don't want to have to rewrite documentation for the same errors being returned by different handlers.I feel like the current incarnation of the code has solved these two problems.
However, I still haven't figured out a good solution to the following problems:
HasServer
instance for Throwing
is somewhat nasty.@cdepillabout, that’s really neat. The use of extensible sum types is really smart, and it seems like they fit in pretty naturally here. The lack of short circuiting is unfortunate, but it seems like that could be implemented using a different handler monad using Nat
?
Do you think servant-checked-exceptions is in a place where I could give it a try? Even if it’s highly unstable, I’d still be interested in just playing with it a bit.
@lexi-lambda I've released servant-checked-exceptions
to hackage. It should be usable. You can find some examples of using it in the README.md in the github repo:
https://github.com/cdepillabout/servant-checked-exceptions
As far as short circuiting is concerned, I created an issue about it. I'm not sure of a good way to make it work (easily) with servant, but if you have any ideas I'd definitely be interested in hearing them.
ZuriHac note: This is advanced-expert task. See Ambitious : Typed errors in https://github.com/haskell-servant/servant/issues/732#issuecomment-297330770
I've updated the servant-checked-exceptions to be able to easily set the HTTP Status for errors.
A simple example would look like the following. This is similar to what @lexi-lambda and @jkarni were describing:
import Servant.Checked.Exceptions (Envelope, ErrStatus(..), Throws, pureSuccEnvelope, pureErrEnvelope)
data BadInputErr = BadInputErr
instance ErrStatus BadInputErr where
toErrStatus :: BadInputErr -> Status
toErrStatus _ = status400
data CapsErr = CapsErr
instance ErrStatus CapsErr where
toErrStatus :: CapsErr -> Status
toErrStatus _ = status404
type API = Capture "foobar" Text :> Throws BadInputErr :> Throws CapsErr :> Get '[JSON] Text
server :: ServerT API Handler
server = foobarEndpoint
foobarEndpoint :: Text -> Handler (Envelope '[BadInputErr, CapsErr] Text)
foobarEndpoint "hello" = pureSuccEnvelope "bye"
foobarEndpoint "HELLO" = pureErrEnvelope CapsErr
foobarEndpoint _ = pureErrEnvelope BadInputErr
app :: Application
app = serve (Proxy :: Proxy Api) server
I posted this in a previous comment, but to rehash on the benefits of using servant-checked-exceptions
:
servant-checked-exceptions-1.0.0.0
, it is possible to set different HTTP status codes for different exceptions.The following problems still exist:
HasServer
instance for Throwing
is not necessarily as robust as it could be.P.S. servant-checked-exceptions-1.0.0.0
currently only supports servant-0.11
(since that is what we are using at work), but it should be easy to update it to work with servant-0.12
and servant-0.13
as well.
@cdepillabout Would you perhaps feel like writing a cookbook entry for your library?
No problem :) Thanks in advance!
I was encouraged to open this issue based on a brief Twitter conversation 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:
If the client requests
/divide/6/3
in JSON, then the result might look like this:If the client requests HTML, then the result might look like this:
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:Now I can add a case to my
ToJSON
andToHTML
instances to produce different errors upon failure, depending on the requested content type: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 ofExceptT
. Sadly, if I produceServantErr
, 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 toHandler
. 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 returnHandler (StatusCode, a)
instead ofHandler a
. The downside to this, of course, is that it is remarkably non-typesafe, and it means you don’t get short-circuiting behavior ofExceptT
.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: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.