This proposal for encoding-independent exceptions is expressed in C# but is submitted to icerpc because (if adopted) it would serve as a template for all language mappings. Some aspects are also programming language independent.
- All remote exceptions in C# derive from the base exception class IceRpc.RemoteException.
A "remote exception" is an exception that originates in a remote peer and gets transmitted via ice or icerpc to the local peer where it's usually thrown.
public abstract class RemoteException : System.Exception // not defined in Slice
{
// nothing remarkable
}
- There are several kinds of remote exceptions:
- invocation exceptions
an invocation exception is an exception reported by a client to server during a request invocation. For example, if a client sends a request to a server and encounters an error while sending the request's payload to the server, it "throws" an invocation exception to the server.
enum InvocationErrorCode : ulong // actual range is 0..2^62-1
{
NoError = 0,
... enumerators...
}
public sealed class InvocationException : RemoteException
{
public InvocationErrorCode ErrorCode { get; }
public InvocationException(InvocationErrorCode errorCode)
{
... throws ArgumentException is errorCode is NoError
}
}
An invocation exception is encoding-independent and does not provide any support for Slice encoding.
- dispatch exceptions
A dispatch exception is an exception reported by a server/service to a cllent during a request dispatch. For example, if a Server has no dispatcher set, it throws to the client a DispatchException(ServiceNotFound)
.
Another example is a service receiving a streamed payload from a client and a later portion of this streamed payload can't be decoded. The service can then throw to the client a DispatchException(PayloadDecodingFailed)
.
public readonly record struct RemoteExceptionOrigin // not defined in Slice
{
public string Path { get; init; }
public string Fragment { get; init; }
public string Operation { get; init; }
}
enum DispatchErrorCode : ulong // actual range is 0..2^62-1
{
NoError = 0,
Unspecified = 1,
ServiceNotFound = 2,
OperationNotFound = 3,
... many more ...
}
public sealed class DispatchException : RemoteException // not defined in Slice
{
public bool ConvertToUnspecified { get; set; }
public DispatchErrorCode ErrorCode { get; }
public RemoteExceptionOrigin Origin { get; init; }
}
Like an invocation exception, a dispatch exception is encoding-independent and doesn't provide any support for Slice encoding.
ConvertToUnspecified avoids accidental propagation of dispatch exceptions. When a dispatch exception is received from a remote peer, ConvertToUnspecified is set to true. And when a dispatch exception is about to be sent to a remote peer and ConvertToUnspecified is true, we actually send DispatchException(Unspecified)
.
Exceptions defined in Slice files and generated by the Slice compiler all derive from AnyException, which itself derives from RemoteException:
public class AnyException : RemoteException // AnyException is the base class for Slice-defined exceptions in C#
{
public bool ConvertToUnhandled { get; set; }
public RemoteExceptionOrigin Origin { get; internal set; } // just like DispatchException
public RetryPolicy RetryPolicy { get; }
// Encode/Decode support with SliceEncoder/Decoder
}
Note: a Slice-defined exception IS NOT a dispatch exception because dispatch exceptions can be thrown at times when Slice-defined exceptions cannot be thrown.
- A middleware can throw dispatch exceptions; they are transmitted to the client by IceRPC. The fidelity of this transmission depends on the RPC protocol.
If a middleware throws another kind of exception, such as a Slice exception, the exception gets transmitted to the client as DispatchException(Unspecified)
.
- The Slice engine (and other future encoding engines) must not let its exceptions flow to the middleware chain. The Slice engine is responsible for catching its exceptions and creating an outgoing response with a Slice-encoded payload.
In C#, this means IceRpc.Service (part of the Slice engine) must catch exceptions - especially Slice-defined exceptions - and create an OutgoingResponse from those exceptions. Currently, it doesn't, see:
https://github.com/zeroc-ice/icerpc-csharp/blob/main/src/IceRpc/Service.cs#L122
It's also there that we insert the logic for converting a ConvertToUnhandled==true exception to a Slice-defined UnhandledException:
// Slice
exception UnhandledException // just like today
{
}
The Slice engine also converts other exceptions - such as ArgumentException - into UnhandledException and sends the full stack trace to the Slice client when built Debug.
When the Slice engine catches a DispatchException with ConvertToUnspecified set to true, it converts this exception into an UnhandledException. When ConvertToUnspecified is false, it lets this exception through to the middleware chain.
- Transmission of invocation exceptions and dispatch exceptions over ice
An invocation exception is not transmitted over ice and kills the connection.
A dispatch exception is transmitted as a system exception over ice (replyStatus > 1) with the following mapping:
ServiceNotFound = ObjectNotExist
OperationNotFound => OperationNotExist
other => Unknown, message includes error code value and name if available
Likewise, an ice system exception is "decoded" as a DispatchException with the following mapping:
ObjectNotExist || FacetNotExist => ServiceNotFound
OperationNotExist => OperationNotFound
Unknown, UnknownLocal, UnknownUserException => Unspecified, message includes Unknown's message
- Transmission of invocation exceptions and dispatch exceptions over icerpc
An invocation exception or dispatch exception is transmitted over icerpc's underlying multiplexed transport using a RESET_STREAM or similar frame with an application error code equal to the InvocationErrorCode or DispatchErrorCode. The exception is then recreated as-is on the other side.