grpc

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 8, 2026 License: AGPL-3.0 Imports: 24 Imported by: 0

Documentation

Overview

Package grpc translates errors into gRPC statuses, and back again on the other side of the wire.

MapToGRPC resolves a Go error to a codes.Code, consulting PlatformMapper first and then whatever mappers domains have registered. The interceptors apply that mapping to whatever a handler returns, and DecodeErrorFromStatus reconstructs the original error on the client, so errors.Is keeps matching across a service boundary.

The sentinel set PlatformMapper maps is the same one errors/http's maps, deliberately. A service exposing both transports would otherwise answer one failure with a considered status on one and codes.Unknown on the other, and which the client got would depend on how it happened to connect. Each domain mapper holds the same property for its own sentinels.

Which direction the imports run

This package imports the packages whose sentinels PlatformMapper maps — circuitbreaking, database, idempotency, ratelimiting, requestsigning, and the two search indexes. Every one of them is a primitive, and that is the whole of the list on purpose: this package is a primitive too, so nothing built on those may appear in it.

The tier above maps itself. dataprivacy, identity, links, operations and sessions each export a GRPCMapper holding the cases for their own sentinels, and the import runs from them to here. Anything else with a sentinel a client should act on does the same: declare a mapper beside the sentinel, and register it.

Registration is what makes a mapper reachable. RegisterGRPCErrorMapper appends one; MapToGRPC consults PlatformMapper first, then registered mappers in registration order. RegisterClientSafeSentinels is the companion for the other half of the answer — whether a sentinel's own words reach the client, described below. This module's four are one call that does both, errormappers.Register, which service.Register makes for a service built from a service.Config and a service assembled by hand makes itself, alongside the mappers it declares for its own sentinels. There is deliberately no init doing it: a mapper that installs itself into a process-wide registry by being linked in is a side effect a consumer cannot opt out of.

What a handler returns

PrepareAndLogGRPCStatus is the spelling a handler wants. It logs, traces, maps the code and hands back an error that is still the error it was given: the sentinel chain intact, with the status alongside it rather than rendered into it.

That last part is the whole point. UnaryErrorEncodingInterceptor can only encode the chain it is handed, so a handler that returned status.Errorf(code, "%v", err) — which is what this function itself used to do — hands it a leaf whose only content is a message, and the sentinel is not merely unmatched on the far side but gone. See observability.GRPCStatusError, which is the error type underneath and where the reasoning is written down.

What reaches the client, and what that assumes

The status message is derived from the code rather than from the error's text, which is the whole wrapped chain and can name tables, connection strings, and the permission that was missing. Two things stand in for the code's name. The first is a list of platform sentinels documented as client-safe, whose own wording tells a caller what to do differently without describing the policy behind the refusal, plus whatever a domain has added to it with RegisterClientSafeSentinels. The second is the description a handler passed PrepareAndLogGRPCStatus — a short account of what it was doing, written for this reader by the code that knew — and a client-safe sentinel outranks it, since the sentinel is the more specific of the two. The interceptors have no description to offer for an error a handler returned bare, and fall back to the code.

The full error does still cross the wire, encoded in the status details, and that is what makes the error reconstructable on the far side. It is meant for trusted service-to-service traffic. A server running these interceptors and reachable by untrusted clients needs that detail stripped at the edge — otherwise the internal error text this package took care to keep out of the message is available in the details of the same response.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ClientSafeMessage

func ClientSafeMessage(err error) (string, bool)

ClientSafeMessage reports the words a client may be told for err: the text of the first client-safe sentinel in its chain, and false when there is none.

"First" is a position in the chain, not in the lists. The chain is walked outermost-first — depth-first through a Join, in the order it was joined — and each node is compared against the client-safe sentinels, registered and platform alike; the first node that is one of them supplies the message. So a more specific wrapper outranks what it wraps: a domain sentinel declared as Wrap(platformerrors.ErrUnrecognizedInputValue, "...") and registered speaks with its own words, because the walk reaches it before it reaches the platform sentinel inside it. A bare platform sentinel, or a registered sentinel built with New, is unaffected — there is only one node to match.

The interceptors consult it through clientMessage, for an error a handler returned bare, and so does PrepareAndLogGRPCStatus for the description a handler passed it — which is how identity/grpc and authentication/signin/grpc get this behavior without asking for it, and why neither has to say the word.

It stays exported for the handler that builds its own status by hand, carrying a code the mappers would not pick or a message this package cannot guess. Such a handler still wants a registered sentinel's own words to win over its own description, since the sentinel is more specific and was registered precisely to be quoted. Without this it either re-implements the two lists or its clients read "FailedPrecondition" where a sentinel had something better to say.

func DecodeErrorFromStatus

func DecodeErrorFromStatus(ctx context.Context, err error) error

DecodeErrorFromStatus extracts the EncodedError from gRPC status details (if present) and decodes it so errors.Is() works across the wire. Returns the decoded error, or the original status error if no encoded detail is found.

func MapToGRPC

func MapToGRPC(err error, defaultCode codes.Code) codes.Code

MapToGRPC returns the appropriate gRPC code for known sentinel errors. It tries PlatformMapper first, then each registered domain mapper. Use std errors.Is for matching. Returns defaultCode if no match.

func PrepareAndLogGRPCStatus

func PrepareAndLogGRPCStatus(err error, logger logging.Logger, span tracing.Span, defaultCode codes.Code, descriptionFmt string, descriptionArgs ...any) error

PrepareAndLogGRPCStatus derives the gRPC code via MapToGRPC, then logs, traces, and returns a status error. defaultCode is the fallback for an error no mapper claims.

This is the spelling a handler holding an observability.Operation wants: observability's function of the same name takes the code it is handed and cannot map, because this package imports it and the reverse edge is a cycle. Logger and Span are on Operation so that reaching this one costs nothing:

grpcerrors.PrepareAndLogGRPCStatus(err, op.Logger(), op.Span(), codes.Internal, "doing the thing")

What comes back is still the error that went in. The chain is intact under a status, not rendered into one, so UnaryErrorEncodingInterceptor has a chain to encode and a client's errors.Is matches the sentinel a handler returned. See observability.GRPCStatusError for what that costs and why the message is the description rather than the chain.

The code is a default in a second sense too: the interceptor re-runs MapToGRPC over the chain this preserves, so a mapper registered after a handler guessed still wins. The message follows ClientSafeMessage — a registered client-safe sentinel's own words outrank the description, since the sentinel is more specific and was registered precisely to be quoted.

func RegisterClientSafeSentinels

func RegisterClientSafeSentinels(sentinels ...error)

RegisterClientSafeSentinels records errors whose own message the interceptors may put on the wire verbatim, rather than the generic string a gRPC code renders as.

It is the companion to RegisterGRPCErrorMapper and answers the other half of the same question: the mapper decides the status, this decides whether the status carries the sentinel's own words. Registering a mapper without these is the usual case — most sentinels describe the system rather than the caller — and a sentinel whose text names a table, a key, or a policy must not be registered here at all.

This module's own sets are links.ClientSafeSentinels and identity.ClientSafeSentinels, and errormappers.Register hands both over alongside the five mappers, so a consumer registering the domain tier gets both halves in one call.

It is additive and safe to call from more than one goroutine, and a sentinel registered twice costs a second comparison and nothing else.

func RegisterGRPCErrorMapper

func RegisterGRPCErrorMapper(m GRPCErrorMapper)

RegisterGRPCErrorMapper registers a domain-specific error mapper, which MapToGRPC consults after PlatformMapper and in registration order.

A package that owns sentinels declares the mapper — dataprivacy.GRPCMapper and its three counterparts in this module are the pattern — and the composition root registers it. For this module's four that is one call, errormappers.Register, which service.Register makes for a service built from a service.Config and a service assembled by hand makes itself; this function is what a consumer calls for a mapper of its own. Doing it from an init function is a consumer's choice to make and not this module's, because a mapper that installs itself by being linked in is a side effect nothing downstream can opt out of.

func StreamErrorEncodingInterceptor

func StreamErrorEncodingInterceptor() grpc.StreamServerInterceptor

StreamErrorEncodingInterceptor returns a stream interceptor that encodes handler errors into gRPC status details for wire transmission.

func UnaryErrorDecodingInterceptor

func UnaryErrorDecodingInterceptor() grpc.UnaryClientInterceptor

UnaryErrorDecodingInterceptor is DecodeErrorFromStatus as a client interceptor, so a caller gets sentinels back without remembering to ask.

DecodeErrorFromStatus on its own is a function every call site has to wrap its result in, and the one that forgets gets a *status.Error that no errors.Is matches — which reads exactly like a server that failed to encode, and is why the encoding side has been an interceptor from the start and this side was not. The two are now symmetric: the server encodes on the way out, the client decodes on the way in, and the sentinel a store returned is the sentinel the caller compares against.

The error it returns answers to both idioms — see decodedError — because making the decode automatic would otherwise silently break every caller that reads status.Code, which is the more common of the two and the one nobody would think to re-check after installing an interceptor.

Std errors.Is works on what it returns, which is not free — see decodedError's Is method for why it takes one, and what the alternative silently cost.

Unary only, and that is narrower than the encoding side: the server has StreamErrorEncodingInterceptor, so a streaming RPC's error does cross the wire encoded, but nothing on the client decodes it yet. A client of a streaming RPC therefore gets a *status.Error that std errors.Is does not match, and DecodeErrorFromStatus is what such a client calls by hand today.

func UnaryErrorEncodingInterceptor

func UnaryErrorEncodingInterceptor() grpc.UnaryServerInterceptor

UnaryErrorEncodingInterceptor returns a unary interceptor that encodes handler errors into gRPC status details for wire transmission. Handlers should return errors (optionally wrapped); the interceptor will derive the gRPC code via MapToGRPC and attach the encoded error to details.

Types

type GRPCErrorMapper

type GRPCErrorMapper interface {
	Map(err error) (code codes.Code, ok bool)
}

GRPCErrorMapper maps domain errors to gRPC codes. ok=false means no match.

var PlatformMapper GRPCErrorMapper = platformMapper{}

PlatformMapper maps platform-level errors to gRPC codes. It does not depend on any domain.

It covers the same sentinel set as the HTTP mapper, and deliberately so: a service exposing both transports would otherwise answer the same failure with a considered status on one and codes.Unknown on the other, and which one a client got would depend on how it happened to connect.

"Platform" is a narrower word here than it looks. It means the primitives — database, circuitbreaking, ratelimiting, idempotency, requestsigning, the two search indexes, and the platformerrors sentinels — and nothing built on them. The mappings for dataprivacy, links, operations and sessions used to live in this switch and now live beside their own sentinels, as dataprivacy.GRPCMapper and its three counterparts, registered with RegisterGRPCErrorMapper. That is what lets this package be depended on by the tier it maps for instead of depending on it, and the both-transports parity above is now a property each of those four packages holds for its own sentinels.

The practical consequence is that those four map only once somebody has registered them. service.Register does it for a service built from a service.Config; a service assembled by hand registers them itself.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL