Server
What AddGrpcErrorHandling registers, how a failure is classified, and the size budget.
ApricotFramework.Grpc.Server is one thing: a failed call answers with classified errors.
builder.Services.AddGrpc(); // gRPC's own registration
builder.Services.AddGrpcErrorHandling(); // ours
app.MapGrpcService<OrdersService>();There is deliberately no AddGrpcServer here. Registering gRPC, mapping services and exposing
reflection are gRPC's own APIs and this library has nothing to add to them — so it does not stand in
front of them, and it leaves the name free for you:
// YourService/Configuration/GrpcServerExtensions.cs
public static IServiceCollection AddGrpcServer(this IServiceCollection services)
{
services.AddGrpc();
services.AddGrpcReflection(); // your call: this exposes the schema
services.AddGrpcErrorHandling();
return services;
}Whether your schema is reachable is a decision about your service, not one a library should make for you — which is the main reason the three are not bundled.
Order does not matter
The interceptor is attached through GrpcServiceOptions, not by wrapping AddGrpc, so it makes no
difference which is called first. Calling AddGrpcErrorHandling twice intercepts once.
How a failure is classified
The interceptor covers all four call shapes. On an exception it asks each registered
IExceptionErrorMapper in registration order, passing the request's HttpContext, and the first
non-empty answer wins:
ErrorDefinitionExceptionMapper, always first, answers with the errors anErrorDefinitionExceptionalready carries — so throwingErr.*(...).AsException()needs no configuration;CancellationExceptionMapperseparates the caller going away (cancelled) from something this service waited on running out of time (timeout);- anything registered with
MapExceptionToError<T>()or a mapper of your own.
These are the same mappers that answer this service's HTTP requests. That is the point: a service says
what its exceptions mean once. AddGrpcErrorHandling calls AddErrorDefinitions for you, which is
idempotent, so a host that already registered it is unaffected.
Note
An RpcException thrown by a service method is left exactly as thrown. A method that has
decided on a status code has said so, and second-guessing it would be worse than useless.
The size budget
GrpcErrorOptions.MaxDetailsBytes bounds the encoded google.rpc.Status, and defaults to 4 KB —
chosen against the 8 KB header list peers commonly allow, leaving room for the other trailers and for
the third that base64 adds:
builder.Services.AddGrpcErrorHandling(options => options.MaxDetailsBytes = 8192);Zero or less sends the status code and message alone. The message is capped at
GrpcErrorOptions.MaxMessageBytes and never takes more than half the budget, so a long message cannot
crowd out the errors it summarises.
Deliberately not bound from configuration: what a service answers when it fails is part of its contract, and a contract an environment variable can change is not one.
Logging
The interceptor is the only thing that sees the exception, so it is the only thing that can record it. A classified failure is logged at debug — a service doing its job. An unrecognised one is logged at error, with the exception, because nothing else will.