Description
GRPC represents big step in microservices communication for speed and performance overall. I am using GRPC with great pleasure and one major advantage for me as a developer is the ease and speed of development because the compiler takes care of generating both the client and the server.
I am personally against using REST as communication between services because for me the simpler the way we access the information, the better. And if are decoupling a monolith into smaller pieces, why shouldn’t I use an RPC style to access information? We can of course create a specifict ApiClient project that encapsulates all the calls to the Api, but it should be easier. Rest use the HTTP status codes to define the state of the operation, this is too generic and not targeted and i want my methods to be preety clear.
I started testing GRPC and it’s all nice but I was having issues with handling server exceptions on the client side: See the example below
The example i am using has 3 projects
- A common project which for simplicity i am using to store common data
- A Grpc server
- A Grpc Client (console application)
The server is the simple SayHello example that is shipped as a template with the GRPC project template in Visual Studio. I want to validate each request that comes into the server using Fluent Validation and if the models are not valid I am throwing an exception. Creating an error model and sending validation messages part of the response should for me is not an option because :
- Errors are not part of the business logic and we should not work with them
- Validation Failures are an exceptional situation where the state is invalid and should be handled “exceptionally” but not part of the business logic flow
The validator
The Client is simple, just calling a method and catching exceptions
Expectation vs reality
My initial expectation was to be able to catch the exception in the client and see the failure messages. But … it was a because i was still thinking that i am just calling a method. There is a channel and a protocol that handles messages between end points and from this point of view it’s no different then WebApi with Http. We need to determine how to pass these error messages.
The RpcException meets the proto standards, it’s a simple model with basic information of the error (the error is of course logged, but in this situation we need more details)
And of course, reading the documentation a bit i understood that there are standards in sending errors back and forward using proto messaging, and the error model is limited.
If with WebApi we would send a BadRequest with a serialized JsonModel of the errors. A simple google and i find that passing errors with metadata is actually encouraged
Solution
For some exceptions I want to get the content and recreate the exception on the client side. For this I created an interceptor and a serializer logic that takes care of grabbing a minimal content:
The interceptor
Will catch the exception and use the exception handler to set up metadata content (if the exception has a serializer attached)
The Exception Handler
The SetException method looks for the custom attribute which must be placed over the exception. That custom attribute contains the serializer type.
- It serializes the content.
- It adds the content in the CapturedExceptionMeta along with the Type of the original exception (Type is not a good practice because it would means sharing resources, you can use a string to describe the content or anything you want)
- It serializes the CapturedExceptionMeta and adds it in the metadata
The TryGetException acts on the client side and it looks for the metadata in the headers and recreates the exception on the client side
public static class ExceptionHandler { //the case is always lowered when receiving metadata public const string METADATA_KEY = "capturedexception"; public static bool SetException(Metadata metadata, Exception exception) { Type actualType = exception.GetType(); IExceptionSerializer exceptionSerializer = GetSerializer(actualType); if (exceptionSerializer != null) { //serialize exception MemoryStream memoryStream = new MemoryStream(); exceptionSerializer.Serialize(exception, memoryStream); memoryStream.Seek(0, SeekOrigin.Begin); byte[] byteContent = memoryStream.ToArray(); //create error model CapturedExceptionMeta exceptionContent = new CapturedExceptionMeta(actualType, byteContent); string serializedExceptionContent = JsonConvert.SerializeObject(exceptionContent); metadata.Add(new Metadata.Entry(METADATA_KEY, serializedExceptionContent)); return true; } return false; } public static Exception TryGetException(Exception exception) { if (exception is RpcException rpcException) { if (rpcException.Trailers != null && rpcException.StatusCode == StatusCode.Unknown && string.Equals(rpcException.Status.Detail, ExceptionHandler.METADATA_KEY, StringComparison.OrdinalIgnoreCase)) { return ExceptionHandler.TryGetException(rpcException.Trailers); } } else { if (exception.InnerException != null) { return TryGetException(exception.InnerException); } } return null; } private static IExceptionSerializer GetSerializer(Type actualType) { IExceptionSerializer exceptionSerializer = null; if (actualType.GetCustomAttribute<ExceptionSerializerAttribute>() is ExceptionSerializerAttribute serializer) { exceptionSerializer = (IExceptionSerializer)Activator.CreateInstance(serializer.SerializerType); } return exceptionSerializer; } private static Exception TryGetException(Metadata metadata) { if (metadata.Any(x => string.Equals(x.Key, METADATA_KEY, StringComparison.OrdinalIgnoreCase))) { //get message string serializedExceptionContent = metadata.SingleOrDefault(x => string.Equals(x.Key, METADATA_KEY, StringComparison.OrdinalIgnoreCase))?.Value; if (serializedExceptionContent == null) { return new ArgumentNullException($"Metadata key with value \'{ExceptionHandler.METADATA_KEY}\' found in the metadata headers but value seems to be null. Could not deserialize into " + $"\'{nameof(CapturedExceptionMeta)}\'"); } CapturedExceptionMeta exceptionContent = JsonConvert.DeserializeObject<CapturedExceptionMeta>(serializedExceptionContent); //read byte content MemoryStream memoryStream = new MemoryStream(exceptionContent.Serialized); memoryStream.Seek(0, SeekOrigin.Begin); //get proper deserializer IExceptionSerializer exceptionSerializer = GetSerializer(exceptionContent.ExceptionType); if (exceptionSerializer == null) { throw new Exception($"Could not handle error content deserialization. Serializer for type {exceptionContent.ExceptionType} not found. Make sure the exception class is " + $"decorated with the attribute {nameof(ExceptionSerializerAttribute)}"); } return exceptionSerializer.Deserialize(memoryStream); } return null; } }
Other files
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] public class ExceptionSerializerAttribute : Attribute { public Type SerializerType { get; } public ExceptionSerializerAttribute(Type serializerType) { SerializerType = serializerType; } } public interface IExceptionSerializer { void Serialize(Exception exception, Stream stream); Exception Deserialize(Stream stream); } public class CapturedExceptionMeta { [JsonProperty("ExceptionType")] public Type ExceptionType { get; } [JsonProperty("Serialized")] public byte[] Serialized { get; } [JsonConstructor] public CapturedExceptionMeta(Type exceptionType, byte[] serialized) { ExceptionType = exceptionType; Serialized = serialized; } }
Cool post thanks a lot. Seems like you forget to close the stream which will leak. I’ve changed some of the code to usings instead hence the streams will be closed when disposed. Also consider using System.Text.Json as it can deserialize from a stream.
Thanks. You are right about the stream leaking. Did not see that…