errdetails

package module
v0.0.0-...-ef3803a Latest Latest
Warning

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

Go to latest
Published: Nov 18, 2021 License: MIT Imports: 21 Imported by: 0

README

Error Detail wrappers

This project provides wrappers around googleapis/rpc/errdetails types, as well as an error type wrapping gRPC status codes.

An error created or wrapped by this package can be unwrapped and transcribed to a Status message type with all the added details intact.

Until inspiration strikes again, all the errors contained herein are derivative of Google's errdetails messages.

Consider unwrapping these errors to enrich logs as well, they're genuinely useful constructs for everyday purposes.

Why, though?

Story time.

We never utilized the WithDetails functionality of gRPC Status messages, or even *status.Status at all, preferring instead to use regular old errors with a translation layer in middleware.

There's so much more knowledge you can express in a Status response coming right from your API layer though, and I wished for some way to make the Status message and arbitrary details more palatable to a team already familiar with error wrapping.

At the same time, I wanted a unified error response body from all our JSON API's, even the ones not transcoded from gRPC.

If only I could just, wrap a Status message with Details like an error.

And if only I could write my wrapped error right through a middleware layer to turn it into JSON.

Fast forward.

Until recently, I was struggling to understand when I should use an interface type in Go. I understood what, and mostly why, but the intuition wasn't all here.

While working on localizations for a project, it started to click a bit. Inspiration hit when I wanted to combine localizability with errors, and maybe also a request ID, and wouldn't ya know it suddenly I couldn't sleep with all the notes I had to write to myself in the "Probably Ideas" category, because errdetails is a thing that exists.

So to reinforce and practice the point till intuition, and also to get it out of the head so I can sleep again, I popped this old idea off the ol' // TODO: list and got to writing something I'll probably never maintain or use, but maybe I will and it'll be nice.

Documentation

Overview

Package errdetails provides a convenient wrapping mechanism to incorporate gRPC Status details with Go's error wrapping paradigm.

Errors provided by this package are implemented by embedding protobuf errdetails messages, and themselves implement an identical interface. This allows the use of protobuf errdetails messages in the method signature of each of the Detail wrappers, any custom implementation thereof, or even another error unwrapped by `errors.As`.

Each error interface can be used in errors.As or errors.Is functions to unwrap, and some enable appending further details this way.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrCanceled           error = &errCodeError{error: errUnknown, Code: codes.Canceled}
	ErrUnknown            error = &errCodeError{error: errUnknown, Code: codes.Unknown}
	ErrInvalidArgument    error = &errCodeError{error: errUnknown, Code: codes.InvalidArgument}
	ErrDeadlineExceeded   error = &errCodeError{error: errUnknown, Code: codes.DeadlineExceeded}
	ErrNotFound           error = &errCodeError{error: errUnknown, Code: codes.NotFound}
	ErrAlreadyExists      error = &errCodeError{error: errUnknown, Code: codes.AlreadyExists}
	ErrPermissionDenied   error = &errCodeError{error: errUnknown, Code: codes.PermissionDenied}
	ErrResourceExhausted  error = &errCodeError{error: errUnknown, Code: codes.ResourceExhausted}
	ErrFailedPrecondition error = &errCodeError{error: errUnknown, Code: codes.FailedPrecondition}
	ErrAborted            error = &errCodeError{error: errUnknown, Code: codes.Aborted}
	ErrOutOfRange         error = &errCodeError{error: errUnknown, Code: codes.OutOfRange}
	ErrUnimplemented      error = &errCodeError{error: errUnknown, Code: codes.Unimplemented}
	ErrInternal           error = &errCodeError{error: errUnknown, Code: codes.Internal}
	ErrUnavailable        error = &errCodeError{error: errUnknown, Code: codes.Unavailable}
	ErrDataLoss           error = &errCodeError{error: errUnknown, Code: codes.DataLoss}
	ErrUnauthenticated    error = &errCodeError{error: errUnknown, Code: codes.Unauthenticated}
)

Known Status Code errors for use as target of errors.Is().

Prefer constructing new errors with New constructor.

Functions

func FromJSON

func FromJSON(r io.Reader, mappers ...DetailsMapper) error

FromJSON reads JSON fom a Reader like a response Body, and makes best effort to reconstruct the wrapped error from gRPC Status such that errors.As and errors.Is may still be satisfied by the error interface types.

For any expected error not already accomodated by this package, you can provide optional DetailsMappers.

If the Map method of a DetailsMapper returns an implementation of Details wrapper, the error is further wrapped by the mapped wrapper.

func New

func New(code codes.Code, msg string, details ...Details) error

New creates a new error from an Status Error Code. Resulting errors can be checked with errors.Is to match exported implementations.

This implementation is specific to gRPC Status codes, but the same example can be applied for any other Flag-like wrapping.

Example
package main

import (
	"errors"
	"fmt"

	"github.com/ClaudiaJ/errdetails"
	"google.golang.org/grpc/codes"
)

func main() {
	// New creates a new error with a distinct Code
	err := errdetails.New(codes.InvalidArgument, "fields not satisfied")

	fmt.Println(errors.Is(err, errdetails.ErrInvalidArgument))
}
Output:

true

func SetErrorHandler

func SetErrorHandler(h ErrorHandler)

SetLogger sets the package level logger that will be used when an error occurs after a Point of No Return and no error may be returned to caller (e.g. while writing to http.ResponseWriter)

By default, these errors are thrown away.

func StreamServerInterceptor

func StreamServerInterceptor(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) (err error)

StreamServerInterceptor transcribes wrapped errors with details into gRPC Status.

func ToJSON

func ToJSON(from error) ([]byte, error)

ToJSON writes an error as JSON with details in-tact such that it can be mostly recovered with FromJSON.

func UnaryServerInterceptor

func UnaryServerInterceptor(ctx context.Context, req interface{}, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error)

UnaryServerInterceptor transcribes wrapped errors with details into gRPC Status.

func WithDetails

func WithDetails(err error, details ...Details) error

WithDetails wrap an error with additional details.

Example
// an error can be enriched with many additional sources of eror details
errdetails.WithDetails(testErr,
	errdetails.BadRequest(),
	errdetails.RequestInfo(&detailspb.RequestInfo{
		RequestId: "123456789",
	}),
)
Output:

Types

type BadRequestError

type BadRequestError interface {
	error
	WithViolation(violation ...details.FieldViolation) BadRequestError
	GetViolations() []details.FieldViolation
}

BadRequestError is an error indicating the client had made a bad request, and includes details of each violation of the field validation rules not satisfied by the request.

func WithBadRequest

func WithBadRequest(err error, violations ...details.FieldViolation) BadRequestError

WithBadRequest wraps an error with Bad Request details having optional field violations.

Example
err := errdetails.WithBadRequest(testErr,
	&detailspb.BadRequest_FieldViolation{
		Field:       "username",
		Description: "username must not contain any part of email address.",
	},
	&detailspb.BadRequest_FieldViolation{
		Field:       "password",
		Description: "password must be at least 5 characters long.",
	},
)

var badReq errdetails.BadRequestError
if errors.As(err, &badReq) {
	fmt.Println("error is", reflect.ValueOf(&badReq).Elem().Type())
	for _, violation := range badReq.GetViolations() {
		fmt.Printf("field violation %q: %s\n", violation.GetField(), violation.GetDescription())
	}
}
Output:

error is errdetails.BadRequestError
field violation "username": username must not contain any part of email address.
field violation "password": password must be at least 5 characters long.

type CausedError

type CausedError interface {
	error
	details.Info
}

CausedError is an error describing the cause of an error with structured details.

func WithCause

func WithCause(err error, info details.Info) CausedError

WithCause wraps an error with information about the cause of the error.

Example
const ReasonThrottle = "UPSTREAM_THROTTLE"

err := errdetails.WithCause(testErr,
	&detailspb.ErrorInfo{
		Reason: ReasonThrottle,
		Domain: "fake.domain.test",
	},
)

var causedErr errdetails.CausedError
if errors.As(err, &causedErr) {
	fmt.Println("error is", reflect.ValueOf(&causedErr).Elem().Type())
	fmt.Printf("with reason %q\n", causedErr.GetReason())
	fmt.Printf("with domain %q\n", causedErr.GetDomain())
}
Output:

error is errdetails.CausedError
with reason "UPSTREAM_THROTTLE"
with domain "fake.domain.test"

type DebugError

type DebugError interface {
	error
	details.DebugInfo
}

DebugError is an error including debug information indicating where an error occurred and any additional details provided by the server.

func WithDebug

func WithDebug(err error, info details.DebugInfo) DebugError

WithDebug wraps an error with additional debugging info.

Example
err := errdetails.WithDebug(testErr,
	&detailspb.DebugInfo{
		StackEntries: []string{"something", "goes", "here"},
		Detail:       "Server responded Internal Server Error with Message wrapping Status as string.",
	},
)

var debugErr errdetails.DebugError
if errors.As(err, &debugErr) {
	fmt.Println("error is", reflect.ValueOf(&debugErr).Elem().Type())
	fmt.Printf("with stack entries: %q\n", debugErr.GetStackEntries())
	fmt.Printf("with detail: %q\n", debugErr.GetDetail())
}
Output:

error is errdetails.DebugError
with stack entries: ["something" "goes" "here"]
with detail: "Server responded Internal Server Error with Message wrapping Status as string."

type Details

type Details interface {
	Wrap(error) error
}

Details are just error wrappers.

func BadRequest

func BadRequest(violations ...details.FieldViolation) Details

BadRequest provides a Details wrapper to enrich errors with BadRequestError details.

Example
package main

import (
	"github.com/ClaudiaJ/errdetails"
	detailspb "google.golang.org/genproto/googleapis/rpc/errdetails"
	"google.golang.org/grpc/codes"
)

func main() {
	errdetails.New(codes.InvalidArgument, "fields not satisfied",
		errdetails.BadRequest(
			// BadRequest takes optional Field Violations describing fields having failed validations.
			&detailspb.BadRequest_FieldViolation{
				Field:       "username",
				Description: "username must not contain any part of email address.",
			},
			&detailspb.BadRequest_FieldViolation{
				Field:       "password",
				Description: "password must be at least 5 characters long.",
			},
		),
	)
}
Output:

func Cause

func Cause(info details.Info) Details

Cause provides a Details wrapper to enrich errors with CausedError details.

Example
package main

import (
	"github.com/ClaudiaJ/errdetails"
	detailspb "google.golang.org/genproto/googleapis/rpc/errdetails"
	"google.golang.org/grpc/codes"
)

func main() {
	errdetails.New(codes.DataLoss, "object payload not received in full",
		errdetails.Cause(&detailspb.ErrorInfo{
			Reason: "stream body prematurely terminated by client",
			Domain: "bucket.platform.test",
		}),
	)
}
Output:

func Code

func Code(code codes.Code) Details

Code wraps an external error with a specified Status Code. Note that while it is possible to wrap an error with multiple status codes, only the outer layer will be considered the resulting Status Code when unwrapped.

func Debug

func Debug(info details.DebugInfo) Details

Debug provides a Details wrapper to enrich errors with DebugError details.

Example
package main

import (
	"github.com/ClaudiaJ/errdetails"
	detailspb "google.golang.org/genproto/googleapis/rpc/errdetails"
	"google.golang.org/grpc/codes"
)

func main() {
	errdetails.New(codes.Internal, "impossible error reached",
		errdetails.Debug(&detailspb.DebugInfo{
			StackEntries: []string{"data.Gnorm/One", "api.Thing/Something"},
			Detail:       "Request body was nil where it shouldn not have been",
		}),
	)
}
Output:

func Help

func Help(links ...details.HelpLink) Details

Help wraps an error with links to documentation or FAQ pages.

Example
package main

import (
	"github.com/ClaudiaJ/errdetails"
	detailspb "google.golang.org/genproto/googleapis/rpc/errdetails"
	"google.golang.org/grpc/codes"
)

func main() {
	errdetails.New(codes.PermissionDenied, "access denied",
		errdetails.Help(&detailspb.Help_Link{
			Url:         "https://login.platform.test/",
			Description: "Login or Register to access this page.",
		}),
	)
}
Output:

func LocalizedMessage

func LocalizedMessage(msg details.LocalizedMessage) Details

LocalizedMessage provides a Details wrapper to enrich errors with LocalizedError details.

Example
package main

import (
	"github.com/ClaudiaJ/errdetails"
	detailspb "google.golang.org/genproto/googleapis/rpc/errdetails"
	"google.golang.org/grpc/codes"
)

func main() {
	errdetails.New(codes.Unavailable, "service in maintenance mode",
		errdetails.LocalizedMessage(
			&detailspb.LocalizedMessage{
				Locale:  "en-US",
				Message: "Mattel Login is down for scheduled maintenance. Please try again later.",
			},
		),
	)
}
Output:

func PreconditionFailure

func PreconditionFailure(violations ...details.PreconditionViolation) Details

PreconditionFailure provides a Details wrapper to enrich errors with FailedPreconditionError details.

Example
package main

import (
	"github.com/ClaudiaJ/errdetails"
	detailspb "google.golang.org/genproto/googleapis/rpc/errdetails"
	"google.golang.org/grpc/codes"
)

func main() {
	errdetails.New(codes.FailedPrecondition, "Terms of Service is required",
		errdetails.PreconditionFailure(
			&detailspb.PreconditionFailure_Violation{
				Type:        "TOS",
				Description: "Please review and acknowledge Terms of Service before continuing.",
			},
		),
	)
}
Output:

func QuotaFailure

func QuotaFailure(violations ...details.QuotaViolation) Details

QuotaFailure provides a Details wrapper to enrich errors with FailedQuotaError details.

Example
package main

import (
	"github.com/ClaudiaJ/errdetails"
	detailspb "google.golang.org/genproto/googleapis/rpc/errdetails"
	"google.golang.org/grpc/codes"
)

func main() {
	errdetails.New(codes.ResourceExhausted, "Too many requests",
		errdetails.QuotaFailure(&detailspb.QuotaFailure_Violation{
			Description: "Rate limit exceeded.",
		}),
	)
}
Output:

func RequestInfo

func RequestInfo(info details.RequestInfo) Details

RequestInfo provides a Details wrapper to enrich errors with RequestInfoError details.

Example
package main

import (
	"github.com/ClaudiaJ/errdetails"
	detailspb "google.golang.org/genproto/googleapis/rpc/errdetails"
	"google.golang.org/grpc/codes"
)

func main() {
	errdetails.New(codes.Internal, "unrecognized mime type on upload",
		errdetails.RequestInfo(&detailspb.RequestInfo{
			RequestId: "123456789",
		}),
	)
}
Output:

func Resource

func Resource(info details.ResourceInfo) Details

Resource provides a Details wrapper to enrich errors with ResourceInfoError details.

Example
package main

import (
	"github.com/ClaudiaJ/errdetails"
	detailspb "google.golang.org/genproto/googleapis/rpc/errdetails"
	"google.golang.org/grpc/codes"
)

func main() {
	errdetails.New(codes.ResourceExhausted, "no more Redlines available",
		errdetails.Resource(&detailspb.ResourceInfo{
			ResourceType: "currency",
			ResourceName: "Redlines",
			Owner:        "auth0|123456789",
			Description:  "no remaining currency in wallet",
		}),
	)
}
Output:

func RetryDelay

func RetryDelay(delay time.Duration) Details

RetryDelay provides a Details wrapper to enrich errors with RetriableError details.

Example
package main

import (
	"time"

	"github.com/ClaudiaJ/errdetails"
	"google.golang.org/grpc/codes"
)

func main() {
	errdetails.New(codes.Unavailable, "upstream responded with temporary failure", errdetails.RetryDelay(time.Minute))
}
Output:

type DetailsMapper

type DetailsMapper interface {
	Map(protoreflect.ProtoMessage) Details
}

DetailsMapper provides a mapping from arbitrary Protobuf message type to an Error wrapper that will reconstruct a fully wrapped error type from JSON data.

This enables error types built ontop protobuf messages not provided within this package to be reconstructed from http response body the same as all of the error types provided by this module.

This will go away whenever I can figure out how to acheive this with protoreflect.

type ErrorHandler

type ErrorHandler interface {
	Handle(error)
}

ErrorHandler handles ierremediable events, e.g. to log error occurring while writing to http.ResponseWriter.

type FailedPreconditionError

type FailedPreconditionError interface {
	error
	WithViolation(...details.PreconditionViolation) FailedPreconditionError
	GetViolations() []details.PreconditionViolation
}

FailedPreconditionError is an error describing what preconditions have failed.

An example being a Terms of Service acknowledgement that may be required before using a particular API or service, responses from the service will indicate that the precondition has not been met.

func WithPreconditionFailure

func WithPreconditionFailure(err error, violations ...details.PreconditionViolation) FailedPreconditionError

WithPreconditionFailure wraps an error describing what preconditions have failed to be met.

Example
err := errdetails.WithPreconditionFailure(testErr, &detailspb.PreconditionFailure_Violation{
	Type:        "TOS",
	Description: "Terms of Service not accepted.",
})

var condErr errdetails.FailedPreconditionError
if errors.As(err, &condErr) {
	fmt.Println("error is", reflect.ValueOf(&condErr).Elem().Type())
	for _, violation := range condErr.GetViolations() {
		fmt.Printf("precondition violation %q: %s\n", violation.GetType(), violation.GetDescription())
	}
}
Output:

error is errdetails.FailedPreconditionError
precondition violation "TOS": Terms of Service not accepted.

type FailedQuotaError

type FailedQuotaError interface {
	error
	WithViolation(...details.QuotaViolation) FailedQuotaError
	GetViolations() []details.QuotaViolation
}

FailedQuotaError is an error describing a quota check failed.

func WithQuotaFailure

func WithQuotaFailure(err error, violations ...details.QuotaViolation) FailedQuotaError

WithQuotaFailure wraps an error describing how a quota check has failed.

Example
err := errdetails.WithQuotaFailure(testErr, &detailspb.QuotaFailure_Violation{
	Subject:     "auth0|123456789",
	Description: "Too many requests, too fast.",
})

var quotaErr errdetails.FailedQuotaError
if errors.As(err, &quotaErr) {
	fmt.Println("error is", reflect.ValueOf(&quotaErr).Elem().Type())
	for _, violation := range quotaErr.GetViolations() {
		fmt.Printf("quota violation %q: %s\n", violation.GetSubject(), violation.GetDescription())
	}
}
Output:

error is errdetails.FailedQuotaError
quota violation "auth0|123456789": Too many requests, too fast.

type HandlerFunc

type HandlerFunc func(http.ResponseWriter, *http.Request) error

HandlerFunc type is an adapter to allow the use of ordinary functions as HTTP handlers.

func (HandlerFunc) ServeHTTP

func (fn HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP serves a JSON error response back to client if the Handler would return an error.

Note of caution: Masking or otherwise distinguishing details safe to share to end client is an exercise left to the implementor.

type HelpfulError

type HelpfulError interface {
	error
	WithLinks(...details.HelpLink) HelpfulError
	GetLinks() []details.HelpLink
}

HelpfulError is an error including links to documentation relevant to the error or API.

func WithHelp

func WithHelp(err error, links ...details.HelpLink) HelpfulError

WithHelp provides a Details wrapper to enrich errors with HelpfulError details.

Example
err := errdetails.WithHelp(testErr, &detailspb.Help_Link{
	Url:         "https://wiki.platform.test/Why_Did_The_Error_Be",
	Description: "Article describing Platform standard errors and troubleshooting",
})

var helpErr errdetails.HelpfulError
if errors.As(err, &helpErr) {
	fmt.Println("error is", reflect.ValueOf(&helpErr).Elem().Type())
	for _, link := range helpErr.GetLinks() {
		fmt.Printf("with url %q:\n%s\n", link.GetUrl(), link.GetDescription())
	}
}
Output:

error is errdetails.HelpfulError
with url "https://wiki.platform.test/Why_Did_The_Error_Be":
Article describing Platform standard errors and troubleshooting

type LocalizedError

type LocalizedError interface {
	error
	details.LocalizedMessage
}

LocalizedError is an error including a localized error message that is safe to return to the user.

func WithLocalizedMessage

func WithLocalizedMessage(err error, msg details.LocalizedMessage) LocalizedError

WithLocalizedMessage wraps an error providing a localized message that's safe to return to the end user.

Example
err := errdetails.WithLocalizedMessage(testErr, &detailspb.LocalizedMessage{
	Locale:  "es-MX",
	Message: "Enviar de nuevo",
})

var locErr errdetails.LocalizedError
if errors.As(err, &locErr) {
	fmt.Println("error is", reflect.ValueOf(&locErr).Elem().Type())
	fmt.Printf("[%s]: %s\n", locErr.GetLocale(), locErr.GetMessage())
}
Output:

error is errdetails.LocalizedError
[es-MX]: Enviar de nuevo

type RequestInfoError

type RequestInfoError interface {
	error
	details.RequestInfo
}

RequestInfoError is an error including metadata about the request that a client can attach when filing a bug or providing other forms of feedback.

func WithRequestInfo

func WithRequestInfo(err error, info details.RequestInfo) RequestInfoError

WithRequestInfo adds RequestID and Serving Data to an error. Generally this is used to serve an ID that correlates with logging, along with encrypted stack trace or similar data relevant for serving a request. Errors then reported by testers or end users can be more readily triaged and troubleshot.

Example
err := errdetails.WithRequestInfo(testErr, &detailspb.RequestInfo{
	RequestId: "123456789",
})

var reqErr errdetails.RequestInfoError
if errors.As(err, &reqErr) {
	fmt.Println("error is", reflect.ValueOf(&reqErr).Elem().Type())
	fmt.Printf("with request ID %q\n", reqErr.GetRequestId())
}
Output:

error is errdetails.RequestInfoError
with request ID "123456789"

type ResourceInfoError

type ResourceInfoError interface {
	error
	details.ResourceInfo
}

ResourceInfoError is an error that describes the resource that is being accessed.

func WithResource

func WithResource(err error, info details.ResourceInfo) ResourceInfoError

WithResource wraps an error with information about the resource that is being accessed.

Example
err := errdetails.WithResource(testErr, &detailspb.ResourceInfo{
	ResourceType: "table",
	ResourceName: "public.shopify",
	Description:  "No record exists in table for shopify URL",
})

var resErr errdetails.ResourceInfoError
if errors.As(err, &resErr) {
	fmt.Println("error is", reflect.ValueOf(&resErr).Elem().Type())
	fmt.Printf("with resource (%s) %q: %s\n", resErr.GetResourceType(), resErr.GetResourceName(), resErr.GetDescription())
}
Output:

error is errdetails.ResourceInfoError
with resource (table) "public.shopify": No record exists in table for shopify URL

type RetriableError

type RetriableError interface {
	error
	WithDelay(time.Duration) RetriableError
	GetRetryDelay() time.Duration
}

RetriableError is an error that describes when a client may retry a failed request.

The retry delay represents a minimum duration in which the client is recommended to wait. It is always recommended the client should use exponential backoff when retrying.

func WithRetryDelay

func WithRetryDelay(err error, delay time.Duration) RetriableError

WithRetryDelay wraps an error indicating that a client may retry a failed request after a delay recommended here.

It is always recommended that clients should use exponential backoff when retrying.

Example
err := errdetails.WithRetryDelay(testErr, 10*time.Minute)

var retErr errdetails.RetriableError
if errors.As(err, &retErr) {
	fmt.Println("error is", reflect.ValueOf(&retErr).Elem().Type())
	fmt.Println("with recommended delay:", retErr.GetRetryDelay())
}
Output:

error is errdetails.RetriableError
with recommended delay: 10m0s

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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