gRPC-haskell icon indicating copy to clipboard operation
gRPC-haskell copied to clipboard

How to return errors to clients?

Open NicolasT opened this issue 8 years ago • 7 comments

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?

NicolasT avatar Sep 08 '17 15:09 NicolasT

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.

ixmatus avatar Sep 08 '17 18:09 ixmatus

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!

NicolasT avatar Sep 09 '17 13:09 NicolasT

@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 avatar Sep 09 '17 14:09 ixmatus

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 <|>.

ixmatus avatar Sep 09 '17 14:09 ixmatus

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 avatar Oct 01 '19 14:10 neongreen

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

neongreen avatar Oct 01 '19 15:10 neongreen

Oh, nevermind, found the solution:

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

neongreen avatar Oct 01 '19 15:10 neongreen