Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow endpoints to advertise returnable errors #349

Closed
pikajude opened this issue Jan 22, 2016 · 6 comments
Closed

Allow endpoints to advertise returnable errors #349

pikajude opened this issue Jan 22, 2016 · 6 comments

Comments

@pikajude
Copy link

When I define an API, I can only advertise what it should return in the success case. But in most cases I also want to be able to tell the user what errors they can expect; for example, a 403 representing auth failure or a 422 representing invalid request data.

I'm not sure how this could be done in practice. My first idea was something like this:

type CommonErrors = '[AuthError, InvalidFoo]

type MyAPI = "foo" :> Get '[JSON] (Foo ': CommonErrors)

and

instance HasServer (Verb method status ctypes rets) where
    type ServerT (Verb method status ctypes rets) m = forall x. IsElem x rets => m x

(though I'm not even sure that code is either possible or logically correct).

Among other things, this could greatly simplify documentation generation.

@fizruk
Copy link
Member

fizruk commented Jan 22, 2016

I think a better approach would be to derive possible errors from the combinators used.
E.g. for this API:

type API = AuthProtected "auth-cookie" :> "foo" :> ReqBody '[JSON] Foo :> Post '[] ()

We know that authentication or request body can fail.
We can define when/how the failure is happening by writing HasServer instances.
We can reflect that in documentation by writing HasSwagger instances for AuthProtected and ReqBody which would insert error responses automatically.

I think this should be the preferred approach.

But sometimes I guess servant users will have some complex logic in their handlers, which they won't be able to easily move to the API. For those cases, you can define a custom combinator like this:

data ExtraErrorResponse status description

instance HasServer api => HasServer (ExtraErrorResponse s d :> api) where
  type ServerT (ExtraErrorResponse s d :> api) = ServerT api
  route _ = route (Proxy :: Proxy api)

instance (KnownNat s, KnownSymbol d, HasSwagger api)
  => HasSwagger (ExtraErrorResponse s d :> api) where
  toSwagger _ = toSwagger (Proxy :: Proxy api)
  & addResponse status response
  where
    status = natVal (Proxy :: Proxy s)
    response = mempty
      & responseDescription .~ Text.pack (symbolVal (Proxy :: Proxy d))

Here's how it can be used for you example:

type family api `WithErrors` errors where
  api `WithErrors` '[] = api
  api `WithErrors` (e ': es) = (e :> api) `WithErrors es

type AuthError = ExtraErrorResponse 403 "Forbidden"
type InvalidFoo = ExtraErrorResponse 400 "Invalid foo"
type CommonErrors = '[AuthError, InvalidFoo]

type MyAPI = "foo" :> Get '[JSON] Foo `WithErrors` CommonErrors

I didn't check any of this code with GHC, though I'm pretty sure it should work.

@pikajude
Copy link
Author

Yes, if there was a way to allow combinators to carry along some context that they can cause a certain type of error, that would be the bee's knees.

@jkarni
Copy link
Member

jkarni commented Jan 22, 2016

There's no safety in this, in the sense that other errors may show up, and those may actually be impossible. Not necessarily damning though, since that's already more or less what happens. We did talk about fixing that a while ago though (some thoughts here); it might be interesting to revisit it.

@jkarni
Copy link
Member

jkarni commented May 11, 2016

@dredozubov in #500 :

I don't know if it was discussed before, but here's my five cents.

Errors are currently produced by the ServantErr record and it would be nice to parametrize it with and error type(and possibly type-level http codes with Nat!) to expose them in an API types:

-- `body` could be constrained by something like `ConformsTo contentType body`
data ServantErr httpCodes body = ServantErr
   { errHTTPCode :: httpCodes
   , errReasonPhrase :: String
   , errBody :: body
   , errHeaders :: [HTTP.Header]
   }

I think it makes a lot of sense, because you can have nice structured errors this way(imagine you have some validation errors which result in the nested json structure). Of course it'll be necessary to add some type-level combinators to make this kind of declarations. For those of you who'll think "just use a sum type for the result API type" - i don't think it captures the http semantics well. You'll have to answer with a error and http code 200 OK this way.

It'll force the changes in one of the two directions:

  1. Handlers will return a sum type ExceptT (ServantErr ErrorTypeExample) IO ResultType
  2. It'll have to run in a slightly different monad

But if it comes this way, it'll be possible to infer all possible(tbh except the parse errors) http codes and error types from the API type.

@domenkozar
Copy link
Contributor

Duplicates #841

@alpmestan
Copy link
Contributor

Indeed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants