Documentation
¶
Overview ¶
Package rpc is the protocol-neutral RPC vocabulary for elephant services: error construction and inspection, the translation between Connect and Twirp errors, and the server and client interceptors that give a Connect mount the same authorization, logging and metrics behaviour the Twirp hooks give a Twirp mount.
The neutral error type is *connect.Error. A handler constructs one through the helpers here, the Connect mount returns it untouched, and the Twirp mount translates it with TwirpInterceptor. Error metadata, which Connect has no free-form place for in the response body, travels as an ErrorMeta detail and is flattened back into Twirp's meta map by that translation.
A service that has not yet moved its handlers off Twirp errors adds LegacyTwirpErrors to its Connect interceptors, which translates the other way, and removes it in the pull request that flips the handlers.
Wrapping a coded error ¶
Do not. Both stacks find a coded error anywhere in the error tree, so the code survives a wrapping, but nothing else does: the caller is answered with the inner error's message, so the wrapper's prefix is written and never read, and Meta and WithMeta look at the outermost coded error only. An error built as Errorf(code, "load the document: %w", inner) therefore answers with the outer code and the inner message, and reports no metadata even when inner carried some.
That last part is deliberate rather than an oversight: re-coding an error is a decision about what the caller is told, and carrying the inner error's metadata out under a different code would tell them something else. Put the context in the message passed to the helper, and add the metadata the caller should see with WithMeta.
The package deliberately does not import the elephantine root package: the root package's APIServer and ServiceOptions use these interceptors, so the dependency runs the other way. The types both packages need are declared in internal packages and aliased here, so rpc.AuthInfo and elephantine.AuthInfo are the same type.
Index ¶
- Constants
- Variables
- func AlreadyExists(msg string) error
- func AuthInfoInterceptor(required bool) connect.Interceptor
- func Errorf(code connect.Code, format string, a ...any) error
- func FailedPreconditionf(format string, a ...any) error
- func FromTwirp(err error) error
- func HTTPStatus(code connect.Code) int
- func Internalf(format string, a ...any) error
- func InvalidArgument(argument string, msg string) error
- func InvalidArgumentf(argument string, format string, a ...any) error
- func IsCode(err error, code connect.Code) bool
- func LegacyTwirpErrors() connect.Interceptor
- func LogErrorResponse(ctx context.Context, logger *slog.Logger, err error)
- func LoggingInterceptor(logger *slog.Logger) connect.Interceptor
- func Meta(err error) map[string]string
- func MetricsInterceptor(reg prometheus.Registerer, opts ...MetricsOption) (connect.Interceptor, error)
- func NotFound(msg string) error
- func OutgoingHeaders(ctx context.Context) http.Header
- func PermissionDeniedf(format string, a ...any) error
- func PropagateHeaders() connect.Interceptor
- func ProtocolLabel(protocol string) string
- func RequiredArgument(argument string) error
- func ResponseCode(err error) connect.Code
- func ResponseStatus(err error, protocol string) string
- func ToTwirp(err error) error
- func TwirpInterceptor() twirp.Interceptor
- func Unauthenticated(msg string) error
- func WithMeta(err error, key string, value string) error
- func WithOutgoingHeaders(ctx context.Context, h http.Header) context.Context
- type AuthInfo
- type ErrorMeta
- type JWTClaims
- type MetricsOption
- type MetricsOptions
Constants ¶
const MetaRequiredScopes = "required_any_of_scopes"
MetaRequiredScopes is the error metadata key that carries the scopes a permission denied error from RequireAnyScope would have accepted.
Variables ¶
var File_rpc_errormeta_proto protoreflect.FileDescriptor
Functions ¶
func AlreadyExists ¶
AlreadyExists creates an already exists error.
func AuthInfoInterceptor ¶
func AuthInfoInterceptor(required bool) connect.Interceptor
AuthInfoInterceptor refuses a call that reaches a handler with no authenticated caller on its context, for a service that requires authentication.
The authentication middleware in elephantine.ServiceOptions answers such a request itself, so this is the safety net for a mount that does not run the middleware: without it a handler would run unauthenticated and have to discover that for itself. It covers streaming handlers as well as unary ones, which is why it is not a connect.UnaryInterceptorFunc.
Pass required as false for a service that allows anonymous callers; the interceptor is then a pass-through, and it is the handler's scope check that decides what an anonymous caller may do.
func Errorf ¶
Errorf creates an error with the given code and a formatted message. A "%w" verb in the format wraps the error it formats, so errors.Is and errors.As keep reaching it through the returned error.
func FailedPreconditionf ¶
FailedPreconditionf creates a failed precondition error with a formatted message. A "%w" verb in the format wraps the error it formats.
Note that Connect answers a failed precondition with HTTP 400, where Twirp answered with 412.
func FromTwirp ¶
FromTwirp translates a Twirp error to a *connect.Error: the code through the map above, the message as it stands, and the meta map key for key into an ErrorMeta detail. The Twirp error is kept as the cause so that errors.Is and errors.As reach whatever it wrapped, a pgx or a context error included.
An error that is already a *connect.Error, and an error that is neither, are returned unchanged: Connect gives the latter the unknown code, which is what it would have done without this translation.
func HTTPStatus ¶
HTTPStatus returns the HTTP status code Connect responds with for an RPC code. It differs from the Twirp status for three codes: canceled is 499 rather than 408, deadline_exceeded is 504 rather than 408, and failed_precondition is 400 rather than 412.
func Internalf ¶
Internalf creates an internal error with a formatted message. A "%w" verb in the format wraps the error it formats.
func InvalidArgument ¶
InvalidArgument creates an invalid argument error for the named argument. The message is "<argument> <msg>" and the argument name is carried in the error metadata as "argument", which is the shape twirp.InvalidArgumentError produces.
func InvalidArgumentf ¶
InvalidArgumentf creates an invalid argument error for the named argument with a formatted message, and is the replacement for elephantine.InvalidArgumentf. A "%w" verb in the format wraps the error it formats.
func IsCode ¶
IsCode checks if any error in the tree is an RPC error with the given code. Both *connect.Error and twirp.Error are recognised, so a check does not have to be changed at the same time as the client constructor it inspects the errors of.
func LegacyTwirpErrors ¶
func LegacyTwirpErrors() connect.Interceptor
LegacyTwirpErrors translates the Twirp errors a handler returns to Connect errors. It is for a service that serves Connect before its handlers have been moved to the Connect error vocabulary, and is removed in the change that moves them.
func LogErrorResponse ¶
LogErrorResponse logs an error response with the keys and levels elephantine.LoggingHooks and LoggingInterceptor use. Middleware that answers a request before it reaches a handler chain — the authentication middleware in elephantine.ServiceOptions is the one in the fleet — logs the refusal with it, so that a refused request is logged the way a refused call is.
The level follows the HTTP status the code is answered with: a bad request or a not found is informational, a server fault is an error, and everything else is a warning. An error that is not a *connect.Error but that wraps a context cancellation or a deadline is logged as canceled or deadline_exceeded, which is how Connect answers it, rather than as an unknown server fault.
func LoggingInterceptor ¶
func LoggingInterceptor(logger *slog.Logger) connect.Interceptor
LoggingInterceptor sets the service, method and subject log metadata for the request and logs error responses. It is the Connect counterpart of elephantine.LoggingHooks and reports the same keys at the same levels, so a dual-stack service's logs do not depend on the protocol the caller used.
func Meta ¶
Meta returns the error metadata of an RPC error: the ErrorMeta detail of a *connect.Error, or the meta map of a twirp.Error. It returns nil if the error carries no metadata.
func MetricsInterceptor ¶
func MetricsInterceptor( reg prometheus.Registerer, opts ...MetricsOption, ) (connect.Interceptor, error)
MetricsInterceptor observes rpc_requests_total, rpc_duration_seconds, rpc_responses_total and rpc_protocol_responses_total for the handlers it wraps. The first three are the series elephantine.NewTwirpMetricsHooks has always reported, with the same names, labels and label values, and the collectors are shared with the hooks, so a service serving both protocols registers each metric once and its dashboards keep working.
The status label is the HTTP status Connect answers the code with, which for canceled, deadline_exceeded and failed_precondition is not the status Twirp answered with. It is 200 for a gRPC or a gRPC-Web response, since those protocols answer every call with 200 and carry the code in the trailers; rpc_protocol_responses_total is where their outcome is readable.
A call that failed authentication is counted as a response but not as a request, which is also what the hooks report, so the difference between the two series means the same thing on both stacks. A request the authentication middleware refuses never reaches an interceptor at all; the middleware counts that response itself, the same way.
Framework-level failures on the Connect stack are not counted here, because connect-go answers them before it calls an interceptor: a malformed body, an unsupported content type, an unknown method and an oversized body are all answered by the protocol handler. Twirp reports those through its hooks, so the two stacks differ for requests that never reach a method.
func OutgoingHeaders ¶
OutgoingHeaders returns the headers set on the context with WithOutgoingHeaders, or nil if it carries none.
func PermissionDeniedf ¶
PermissionDeniedf creates a permission denied error with a formatted message. A "%w" verb in the format wraps the error it formats.
func PropagateHeaders ¶
func PropagateHeaders() connect.Interceptor
PropagateHeaders is the client interceptor that applies the headers set with WithOutgoingHeaders to the outgoing request. Pass it to a generated client constructor as connect.WithInterceptors(rpc.PropagateHeaders()).
It has to be an interceptor rather than something the generated client does, because the generated clients are compiled from elephant-api, which must not depend on elephantine.
func ProtocolLabel ¶
ProtocolLabel maps connect-go's protocol name to the protocol label value. Only the spelling of gRPC-Web differs; a protocol connect-go adds later is bucketed as "other" rather than put into the label space unannounced.
func RequiredArgument ¶
RequiredArgument creates an invalid argument error for an argument that must be set.
func ResponseCode ¶
ResponseCode is the code Connect answers an error with. A *connect.Error carries its own code; an error that is not one but that wraps a context cancellation or a deadline is answered canceled or deadline_exceeded, the way connect-go itself codes it; anything else is unknown.
The context codes matter because connect-go returns a bare context error from its own handler wrapper when the request context is already done, so a caller that disconnected or that ran out of its Connect-Timeout-Ms would otherwise be counted and logged as an unknown server fault.
func ResponseStatus ¶
ResponseStatus is the HTTP status a Connect response is sent with, as a string, which is the form twirp.StatusCode reports it in. gRPC and gRPC-Web answer every call with 200 and carry the code in the trailers, so a response on those protocols is reported as 200 whatever the error was.
It is the Connect status mapping, which differs from Twirp's for canceled, deadline_exceeded and failed_precondition, so a caller reporting a Twirp response has to know that those three codes are the ones it cannot use it for. The authentication middleware can: the codes it answers with are answered with the same status on both stacks.
func ToTwirp ¶
ToTwirp translates a *connect.Error to a twirp.Error: the code through the map above, the message as it stands, and the ErrorMeta detail flattened back into the Twirp meta map key for key. Details of any other type are dropped, since Twirp has nowhere to render them, and logged at debug level with their message name. The cause chain is preserved.
A twirp.Error is returned unchanged, and any other error becomes twirp.InternalErrorWith, which is what the generated Twirp server does with an uncoded error, so nothing about uncoded errors changes.
func TwirpInterceptor ¶
func TwirpInterceptor() twirp.Interceptor
TwirpInterceptor translates the errors a handler returns to Twirp errors, so that a handler written against the Connect error vocabulary answers a Twirp caller with the code, message and meta it has always answered with. Install it with twirp.WithServerInterceptors; elephantine.ServiceOptions.ServerOptions does that for you.
An interceptor rather than a hook, because the error hook only sees the error Twirp has already decided on.
func Unauthenticated ¶
Unauthenticated creates an unauthenticated error, for a caller we could not identify. Use PermissionDeniedf for a caller we could identify but that is not allowed to perform the operation.
func WithMeta ¶
WithMeta returns a copy of the error with the given key/value pair added to its ErrorMeta detail, creating the detail if the error does not have one. An error that is not a *connect.Error is given the unknown code, which is what Connect would have given it anyway.
The returned error is the *connect.Error itself, so anything the error was wrapped in is dropped.
func WithOutgoingHeaders ¶
WithOutgoingHeaders creates a child context carrying HTTP headers to set on the requests made through a client that has the PropagateHeaders interceptor. It replaces twirp.WithHTTPRequestHeaders, and like it, it replaces rather than merges: a second call decides the headers.
The headers are copied, so a later change to h does not reach the requests.
Types ¶
type AuthInfo ¶
AuthInfo is the authentication information of the calling client. It is an alias of elephantine.AuthInfo, not a separate type.
func GetAuthInfo ¶
GetAuthInfo returns the authentication information for the given context. It is the same function as elephantine.GetAuthInfo, repeated here so that a service that has moved to this package does not have to import both.
func RequireAnyScope ¶
RequireAnyScope checks that the authenticated caller carries one of the named scopes. On success it returns the AuthInfo from the context; on failure it returns an error suitable for direct return from an RPC handler. An anonymous caller (no AuthInfo, or an empty subject) yields unauthenticated; an authenticated caller without any of the required scopes yields permission_denied with the accepted scope list in the error metadata under "required_any_of_scopes".
Scopes are OR-ed: passing more than one means the caller may hold any of them.
type ErrorMeta ¶
type ErrorMeta struct {
Meta map[string]string `` /* 135-byte string literal not displayed */
// contains filtered or unexported fields
}
ErrorMeta carries free-form key/value error metadata. It replaces the Twirp error meta map and is flattened back into it by the Twirp adapter.
func (*ErrorMeta) Descriptor
deprecated
func (*ErrorMeta) ProtoMessage ¶
func (*ErrorMeta) ProtoMessage()
func (*ErrorMeta) ProtoReflect ¶
func (x *ErrorMeta) ProtoReflect() protoreflect.Message
type JWTClaims ¶
JWTClaims are the claims the elephant services understand. It is an alias of elephantine.JWTClaims, not a separate type.
type MetricsOption ¶
type MetricsOption func(opts *MetricsOptions)
MetricsOption configures the interceptor created by MetricsInterceptor.
func WithMetricsCustomerFunc ¶
func WithMetricsCustomerFunc(fn func(ctx context.Context) string) MetricsOption
WithMetricsCustomerFunc sets a function that returns the customer label value for a context. It is the Connect counterpart of elephantine.WithTwirpMetricsCustomerFunc, and a dual-stack service passes the same function to both so the label means the same thing on both stacks.
func WithMetricsStaticTestLatency ¶
func WithMetricsStaticTestLatency(latency time.Duration) MetricsOption
WithMetricsStaticTestLatency makes the interceptor report a static duration, so that a test can assert on the histogram.
type MetricsOptions ¶
type MetricsOptions struct {
// contains filtered or unexported fields
}
MetricsOptions holds the configuration for the Connect metrics interceptor created by MetricsInterceptor. Set it through MetricsOption functions.