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

How to return errors to clients? #32

Open
NicolasT opened this issue Sep 8, 2017 · 7 comments
Open

How to return errors to clients? #32

NicolasT opened this issue Sep 8, 2017 · 7 comments

Comments

@NicolasT
Copy link

NicolasT commented Sep 8, 2017

It's a bit unclear to me how one is supposed to return an error to a client for a 'normal' RPC request, except by throwing an exception in a handler (which is quite ugly, because then said error gets logged as well, which may not be useful at all).

A handler is supposed to return a ServerResponse 'Normal respType, which is ServerNormalResponse :: respType -> MetadataMap -> StatusCode -> StatusDetails -> ServerResponse 'Normal respType. When a handler fails, i.e. is unable to calculate a proper value of respType, how is one supposed to return a response with e.g. StatusAborted to the client?

@ixmatus
Copy link
Contributor

ixmatus commented Sep 8, 2017

My colleague @intractable solved this by writing monoid instances for the rpc response types. gRPC and proto3 lean heavily on "zero initialized" value semantics for the scalar types and some composite types (like enums and repeated, I think, but not for message types whose "missing value" semantics are language-dependent).

The missing value for message types, in Go, is rendered using a pointer type. Our Haskell code generator renders it with a Maybe type. Therefore if you want Maybe-like semantics for fields of a response that are of a scalar type, you need to wrap those scalar types with a message type (i.e: "newtype" it).

If you do this, then it's pretty easy to define what the identity for your monoid instance is. Once you have that then you can simply use mempty. Which is convenient.

For example:

ServerNormalResponse mempty mempty StatusAborted (StatusDetails "my error message")

Barring that. Knowing that scalar types have zero initialized values and that fields with a message type can be missing, you could construct a response type that is "zero initialized" and use that in your error response.

If you wished to return richer information in a response that is not StatusOK, you will have to account for that in the design of your gRPC API types though we've found the StatusDetails to usually be good enough. We're working on supporting OneOf and once that is complete I could see that being used for an Either-like response if you wanted to return more information in the response message and not just StatusDetails.

@NicolasT
Copy link
Author

NicolasT commented Sep 9, 2017

Well, the question didn't really stem from "Some fields can't be initialized", rather "There's no sensible value to return".

Indeed, I could model this within the RPC spec, but then all my calls would return some kind of Either Error response value. This has some elegance, I guess... I'll consider it.

Mentioning Monoid/mempty (which I can't implement for my response types, there's no real sensible value for mappend) made me realize I do have a 'whatever' value at hand: my types also implement Data.Default.Class.Default (for other reasons), so I can simply use def.

Anyway: there must be some way to return an error to a client without including a full response value, otherwise how does grpc-haskell handle exceptions in request handlers? Exposing this (by adding a ServerErrorResponse :: MetadataMap -> StatusCode -> StatusDetails -> ServerResponse 'Normal a or so?) to library users could be useful...

Thanks for the info!

@ixmatus
Copy link
Contributor

ixmatus commented Sep 9, 2017

@NicolasT I misinterpreted your question then, sorry.

You can return an error from the server with a status code and description using a type of GRPCIOError with this constructor: GRPCIOBadStatusCode C.StatusCode C.StatusDetails. GRPCIOError is an instance of Exception.

AFAIK, you produce a value of type GRPCIOError by raising an exception.

The client will receive that information in a type of ClientError. Where you can match on the GRPCIOBadStatusCode.

If you wish to return a response without it being delivered as a ClientError then you will need to do something along the lines of what we've already talked about (I could be wrong, @intractable and @crclark have contributed the most amount of code to this project, so hopefully they can weigh in too). Having default instances for your response message type is another way you could return a "zero initialized" response message, that seems pretty reasonable to me.

@ixmatus
Copy link
Contributor

ixmatus commented Sep 9, 2017

Oh one more thought w.r.t selection of a monoid for grpc proto response messages: the monoid @intractable selected for the response types implements the mappend method in terms of <|>.

@neongreen
Copy link

neongreen commented Oct 1, 2019

AFAIK, you produce a value of type GRPCIOError by raising an exception.

Doesn't seem to work. For instance, if I do this on the server side:

    throw $
        GRPCIOBadStatusCode 
            StatusUnknown
            "rate limit descriptor list must not be empty"

I get this on the client side:

    ClientIOError
        (GRPCIOBadStatusCode
             StatusCancelled (StatusDetails "Cancelled"))

@neongreen
Copy link

(What I would like to get is ClientIOError (GRPCIOBadStatusCode StatusUnknown (StatusDetails "rate limit descriptor list must not be empty")).)

@neongreen
Copy link

Oh, nevermind, found the solution:

method (ServerNormalRequest serverCall request) = do
        ...
        serverCallCancel
            serverCall
            StatusUnknown
            "rate limit descriptor list must not be empty"

RichardWarfield pushed a commit to litxio/gRPC-haskell that referenced this issue Apr 25, 2023
GHC 8.2 became more restrictive when checking default signatures, demanding an
explicit equality constraint here. See also:

ekmett/lens#712

Referenced there:
https://ghc.haskell.org/trac/ghc/ticket/13258
https://ghc.haskell.org/trac/ghc/ticket/13249
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants