Help us shape the future of Go! Take the 2026 Go Developer Survey

grpc

package
v2.17.1 Latest Latest
Warning

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

Go to latest
Published: Oct 16, 2026 License: AGPL-3.0 Imports: 26 Imported by: 111

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 second half of the answer — whether a sentinel's own words reach the client — and RegisterClientSafeReasons for the third, whether the response also carries a stable identifier a client can branch on. All three are described below. This module's four are one call that does them together, 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. StripEncodedErrorDetail is that edge's spelling: it removes exactly the encoded detail and leaves the rest of the response alone. StripEncodedErrorDetailUnaryServerInterceptor and StripEncodedErrorDetailStreamServerInterceptor apply it to everything a server returns; chain them outside the encoding interceptors.

The third channel

Neither of those two gives a client something to switch on. The message is prose, written for a person and rewordable at any time; the details are the peer's and are supposed to be stripped before a client sees them. A caller that has to branch on which refusal it got — show a second-factor prompt for one Unauthenticated, a password field for another — has historically had only the English sentence, which makes the sentence a public interface nobody may reword and every client matches differently.

RegisterClientSafeReasons closes that. A registered sentinel carries a stable UPPER_SNAKE_CASE identifier, which the interceptors emit in a google.rpc.ErrorInfo detail — distinct from the encoded-error detail, so "strip the peer detail, keep the client detail" is something an edge can express. Registering a reason also registers the sentinel as client-safe, since both halves say the same thing about the same refusal. ClientReasonFromStatus is the client's read, and it needs neither the encoded detail nor the decoding interceptor.

This discloses nothing new: anything that can read a reason can already read the message saying the same thing in prose. It only narrows what a client depends on.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ClientReasonFromStatus added in v2.6.0

func ClientReasonFromStatus(err error) (*errdetails.ErrorInfo, bool)

ClientReasonFromStatus reads the google.rpc.ErrorInfo a server attached, and false when there is none.

This is the client half, and it works on the status as it arrived: it needs neither the encoded detail nor the decoding interceptor, which is the point — a reason survives an edge that stripped the encoded chain, and a client in a language with no cockroachdb/errors can read the same field out of the same detail.

A Go client with UnaryErrorDecodingInterceptor installed can pass what that returns straight in: the decoded error still answers as the status it arrived as, so the detail is still reachable through it.

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 RegisterClientSafeReasons added in v2.6.0

func RegisterClientSafeReasons(reasons ...ClientReason)

RegisterClientSafeReasons records the stable identifiers the interceptors may put on the wire for the sentinels they name.

It is the third of the registration calls, beside RegisterGRPCErrorMapper, which decides the status code, and RegisterClientSafeSentinels, which decides whether the status carries the sentinel's own words. This one decides whether the response also carries something a client can switch on — see ClientReason for why the other two could not.

Registering here registers the sentinel as client-safe too, so a sentinel with a reason does not also need a RegisterClientSafeSentinels call. The two are halves of one statement — this refusal is disclosable, here is what to call it and here is what to say about it — and keeping them in one call is what stops them from drifting apart. The rule RegisterClientSafeSentinels states applies unchanged: a sentinel whose text names a table, a key, or a policy must not be registered here at all, reason or no reason.

An entry with a nil Err or an empty Reason is ignored, since neither half of it can answer anything.

It is additive and safe to call from more than one goroutine, and a sentinel registered twice costs a second comparison and nothing else. Where the same sentinel is registered with two different reasons the first one registered wins, for the same reason the chain walk takes the first match: a later registration reaching back and changing what a client already branches on is the one thing a stable identifier may not do.

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.

A sentinel a client has to branch on rather than merely display goes to RegisterClientSafeReasons instead, which registers it here too and adds the stable identifier this call has nothing to say about.

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.

The same unredacted-details caveat applies — see UnaryErrorEncodingInterceptor.

func StripEncodedErrorDetail added in v2.6.0

func StripEncodedErrorDetail(err error) error

StripEncodedErrorDetail returns err with the encoded-chain detail removed and every other detail left alone.

This is what the third channel bought. The encoded chain is meant for a peer this process trusts, so a server reachable by untrusted clients has to strip it — and until there was a detail worth keeping, "strip it" and "strip the details" were the same sentence, so an edge either dropped everything or read the type URLs itself. This drops exactly the one detail that carries internal text, so the reason a client branches on goes through.

An error that is not a status, or a status with nothing to strip, comes back unchanged, so an edge may call it on everything it forwards. Note that what comes back is a plain status error: the chain the detail carried is gone, by construction, and errors.Is against a sentinel no longer matches it. Strip at the edge, after anything in-process that wanted the sentinel has had it.

func StripEncodedErrorDetailStreamServerInterceptor added in v2.11.0

func StripEncodedErrorDetailStreamServerInterceptor() grpc.StreamServerInterceptor

StripEncodedErrorDetailStreamServerInterceptor is StripEncodedErrorDetailUnaryServerInterceptor for streaming RPCs, and goes outside StreamErrorEncodingInterceptor on the same terms.

func StripEncodedErrorDetailUnaryServerInterceptor added in v2.11.0

func StripEncodedErrorDetailUnaryServerInterceptor() grpc.UnaryServerInterceptor

StripEncodedErrorDetailUnaryServerInterceptor applies StripEncodedErrorDetail to every error a unary RPC returns, which is how a server reachable by untrusted clients strips at its own edge.

Chain it outside UnaryErrorEncodingInterceptor — earlier in the list, since grpc-go runs the first interceptor outermost — or there is nothing yet to strip when it runs. Everything inside it still sees the full chain.

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. The details channel is not redacted, and that is the half of this file worth reading before installing either interceptor.

clientMessage below takes care to keep the wrapped chain — table names, connection strings, the permission that was missing — off the status message. encodeErrorToDetails then puts that same chain into the status details of the same response, because reconstructing the error on the far side is what these interceptors are for. The two are not protecting the same thing and are not meant to: the message is for whoever is calling, the details are for a peer this process trusts.

So a server running these and reachable by untrusted clients — a mobile app, a browser — needs the detail stripped at the edge, or the text the message channel refused to carry is available beside it, and DecodeErrorFromStatus is the supported way to unpack it. The package documentation states this too; it is repeated here because this is the file somebody reads when they are deciding to install one, and clientMessage's own comment reads like an assurance that covers the whole response.

Types

type ClientReason added in v2.6.0

type ClientReason struct {
	// Err is the sentinel, matched the way ClientSafeMessage matches: the
	// outermost node of the chain that is this error supplies the reason.
	Err error
	// Reason is the stable identifier, UPPER_SNAKE_CASE.
	Reason string
	// Domain is the logical grouping the reason belongs to. May be empty.
	Domain string
}

ClientReason is a sentinel and the stable identifier a client branches on when it gets that refusal.

It is the third channel, and the one the other two could not be. The status message is client-safe prose — a registered sentinel's own words, which are written for a person and may be reworded, localized or shortened at any time. The status details carry the whole encoded chain, which is for a peer this process trusts and which a client-facing edge is supposed to strip. Neither gives a client something to switch on: a caller that must show a TOTP prompt for one Unauthenticated and a password field for another has, today, only the English sentence, so the sentence becomes a public interface that cannot be reworded and that every client reinvents its own match against.

Reason is that switch. It is emitted in a google.rpc.ErrorInfo detail, which is distinct from the encoded-error detail on purpose: "strip the peer detail, keep the client detail" is then something an edge can express, rather than the all-or-nothing judgement call stripping is now.

Nothing here widens what a client can learn. Anything that can read a reason can already read the message saying the same thing in prose; this only narrows what a client depends on.

Reason should be UPPER_SNAKE_CASE, which is google.rpc.ErrorInfo's own convention and what tooling in other languages expects. It is a name a client compiles against, so it is chosen once and never reworded — reword the sentinel's message instead, which is what the message channel is for.

Domain is the logical grouping the reason belongs to, conventionally the service that issued it, and it is what makes two services' reasons distinct when a gateway merges them. A closed system that only ever talks to one server may leave it empty.

There is deliberately no Metadata, which google.rpc.ErrorInfo also carries: a value fixed at registration time is the same for every occurrence and says nothing a client could not hardcode beside the reason, and a per-occurrence one would have to be read off the error, which is a different feature.

func ClientSafeReason added in v2.6.0

func ClientSafeReason(err error) (ClientReason, bool)

ClientSafeReason reports the registered reason for err: the record whose Err is the first node of err's chain that has one, and false when no node does.

"First" is the same position ClientSafeMessage means — the chain walked outermost-first, depth-first through a Join in the order it was joined — so a registered wrapper outranks the registered sentinel it wraps, and a chain that resolves to one sentinel for the message resolves to that same sentinel for the reason, since registering a reason registers the message too.

The one way the two channels can name different nodes is a message-only sentinel wrapping a reason-bearing one, where each answers with the most specific node it has. That is the right answer for both — a client reading prose and a client reading an identifier each get the nearest thing their own channel knows about — but it is worth knowing before wrapping a sentinel that has a reason in one that does not.

The interceptors consult it for every error they encode. It stays exported for the handler shaping its own status, which wants ClientReason.Detail, and for an edge that would rather read the reason off the error it already has than off the status it is about to send.

func (ClientReason) Detail added in v2.6.0

func (r ClientReason) Detail() *errdetails.ErrorInfo

Detail renders the reason as the google.rpc.ErrorInfo the interceptors put on the wire.

It is 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 — and wants that status to carry the same detail a handler going through the interceptors gets for free. Such a handler reaches it through ClientSafeReason, which hands back the registered record this is a method on.

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, sms, 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