errors

package
v2.1.6 Latest Latest
Warning

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

Go to latest
Published: Feb 19, 2024 License: Apache-2.0 Imports: 10 Imported by: 0

README

Errors

To avoid burden of mapping errors returned from 3rd party libraries you can gather all error mappings in one place and put an interceptor provided by atlas-app-toolkit package in a middleware chain as following:

interceptor := errors.UnaryServerInterceptor(
	// List of mappings

	// Base case: simply map error to an error container.
	errors.NewMapping(fmt.Errorf("Some Error"), errors.NewContainer(/* ... */).WithDetail(/* ... */)),
)

Contents

  1. Background
  2. Error Mappers
  3. Usage
  4. Validation Errors
  5. PQ Errors

Error Handling

Background

This document is a brief overview of facilities provided by error handling package. The rationale for implementing it are four noble reasons:

  1. Provide ability to add specific details and field information to an error.
  2. Provide ability to handle multiple errors without returning control to a callee.
  3. Ability to map errors from 3-rd party libraries (gorm, to name one).
  4. Mapping from error to container should be performed automatically in gRPC interceptor.

Error Mappers

Error mapper performs conditional mapping from one error message to another. Error mapping functions are passed to a gRPC Error interceptor and called against error returned from handler.

Currently there are two mappers available:

  • ValidationErrors: error mapper and interceptor for protoc-gen-validate validation errors
  • PQErrors: error mapper for go postgres driver

Error Container

Error container is a data structure that implements Error interface and GRPCStatus method, enabling passing it around as a conventional error from one side and as a protobuf Status to gRPC gateway from the other side.

There are several approaches exist to work with it:

  1. Single error mode
  2. Multiple errors mode

Usage

Single Error Return

This code snippet demonstrates the usage of error container as conventional error:

func validateNameLength(name string) error {
	if len(name) > 255 {
		return errors.NewContainer(
			codes.InvalidArgument, "Name validation error."
		).WithDetail(
			codes.InvalidArgument, "object", "Invalid name length."
		).WithField(
			"name", "Specify name with length less than 255.")
	}

	return nil
}
Gather Multiple Errors
func (svc *Service) validateName(name string) error {
	err := errors.InitContainer()
	if len(name) > 255 {
		err = err.WithDetail(codes.InvalidArgument, "object", "Invalid name length.")
		err = err.WithField("name", "Specify name with length less than 255.")
	}

	if strings.HasPrefix(name, "_") {
		err = err.WithDetail(codes.InvalidArgument, "object", "Invalid name.").WithField(
			"name", "Name cannot start with an underscore")
	}

	return err.IfSet(codes.InvalidArgument, "Invalid name.")
}

To gather multiple errors across several procedures use context functions:

func (svc *Service) globalValidate(ctx context.Context, input *pb.Input) error {
	svc.validateName(ctx, input.Name)
	svc.validateIP(ctx, input.IP)

	// in some particular cases we expect that something really bad
	// should happen, so we can analyse it and throw instead of validation errors.
	if err := validateNamePermExternal(svc.AuthInfo); err != nil {
		return errors.New(ctx, codes.Unauthorized, "Client is not authorized.").
			WithDetails(/* ... */)
	}

	return errors.IfSet(ctx, codes.InvalidArgument, "Overall validation failed.")
	// Alternatively if we want to return the latest errCode/errMessage set instead
	// of overwriting it:
	// return errors.Error(ctx)
}

func (svc *Service) validateName(ctx context.Context, name string) {
	if len(name) > 255 {
		errors.Detail(ctx, codes.InvalidArgument, "object", "Invalid name length.")
		errors.Field(ctx, "name", "Specify name with length less than 255.")
	}
}

func (svc *Service) validateIP(ctx context.Context, ip string) { /* ip validation */ }

Error Mapper

Error mapper performs conditional mapping from one error message to another. Error mapping functions are passed to a gRPC Error interceptor and called against error returned from handler.

Below we demonstrate a cases and customization techniques for mapping functions:

interceptor := errors.UnaryServerInterceptor(
	// List of mappings
	
	// Base case: simply map error to an error container.
	errors.NewMapping(fmt.Errorf("Some Error"), errors.NewContainer(/* ... */).WithDetail(/* ... */)),

	// Extended Condition, mapped if error message contains "fk_contraint" or starts with "pg_sql:"
	// MapFunc calls logger and returns Internal Error, depending on debug mode it could add details.
	errors.NewMapping(
		errors.CondOr(
			errors.CondReMatch("fk_constraint"),
			errors.CondHasPrefix("pg_sql:"),
		),
		errors.MapFunc(func (ctx context.Context, err error) (error, bool) {
			logger.FromContext(ctx).Debug(fmt.Sprintf("DB Error: %v", err))
			err := errors.NewContainer(codes.Internal, "database error")

			if InDebugMode(ctx) {
				err.WithDetail(/* ... */)
			}

			// boolean flag indicates whether the mapping succeeded
			// it can be used to emulate fallthrough behavior (setting false)
			return err, true
		})
	),

	// Skip error
	errors.NewMapping(fmt.Errorf("Error to Skip"), nil)
)

Such model allows us to define our own error classes and map them appropriately as in example below:

// service validation code.

type RequiredFieldErr string
(e RequiredFieldErr) Error() { return string(e) }

func RequiredFieldCond() errors.MapCond {
	return errors.MapCond(func(err error) bool {
		_, ok := err.(RequiredFieldErr)
		return ok
	})
}

func validateReqArgs(in *pb.Input) error {
	if in.Name == "" {
		return RequiredFieldErr("name")
	}

	return nil
}
// interceptor init code
interceptor := errors.UnaryServerInterceptor(
	errors.NewMapping(
		RequiredFieldCond(),
		errors.MapFunc(func(ctx context.Context, err error) (error, bool) {
			return errors.NewContainer(
				codes.InvalidArgument, "Required field missing: %v", err
			).WithField(string(err), "%q argument is required.", string(err)
			), true
		}),
	)
)

Validation Errors

import "github.com/lunchroum/atlas-app-toolkit/errors/mappers/validationerrors"

validationerrors is a request contents validator server-side middleware for gRPC.

Request Validator Middleware

This middleware checks for the existence of a Validate method on each of the messages of a gRPC request. In case of a validation failure, an InvalidArgument gRPC status is returned, along with the error that caused the validation failure.

It is intended to be used with plugins like https://github.com/lyft/protoc-gen-validate, Go protocol buffers codegen plugins that create the Validate methods (including nested messages) based on declarative options in the .proto files themselves.

func UnaryServerInterceptor
func UnaryServerInterceptor() grpc.UnaryServerInterceptor

UnaryServerInterceptor returns a new unary server interceptor that validates incoming messages and returns a ValidationError.

Invalid messages will be rejected with InvalidArgument and the error before reaching any userspace handlers.

func DefaultMapping
func DefaultMapping() errors.MapFunc

DefaultMapping returns a mapper that parses through the lyft protoc-gen-validate errors and only returns a user friendly error.

Example Usage:

  1. Add validationerrors and errors interceptors to your application:

    errors.UnaryServerInterceptor(ErrorMappings...),
    validationerrors.UnaryServerInterceptor(),
    
  2. Create an ErrorMapping variable with all your mappings.

  3. Add DefaultMapping as part of your ErrorMapping variable

    var ErrorMappings = []errors.MapFunc{
       // Adding Default Validations Mapping
       validationerrors.DefaultMapping(), 
    
    }
    

    Example return after DefaultMapping on a invalid email:

    {
        "error": {
            "status": 400,
            "code": "INVALID_ARGUMENT",
            "message": "Invalid primary_email: value must be a valid email address"
        },
        "fields": {
            "primary_email": [
                "value must be a valid email address"
            ]
        }
    }
    
  4. You can also add custom validation mappings:

    var ErrorMappings = []errors.MapFunc{
        // Adding custom Validation Mapping based on the known field and reason from lyft
       errors.NewMapping(
    		errors.CondAnd(
    			validationerrors.CondValidation(),
                validationerrors.CondFieldEq("primary_email"),
    			validationerrors.CondReasonEq("value must be a valid email address"),
    		),
    		errors.MapFunc(func(ctx context.Context, err error) (error, bool) {
    			vErr, _ := err.(validationerrors.ValidationError)
    			return errors.NewContainer(codes.InvalidArgument, "Custom error message for field: %v reason: %v", vErr.Field, vErr.Reason), true
            }),
       ),
    }
    
    

PQ Errors

import "github.com/lunchroum/atlas-app-toolkit/errors/mappers/pqerrors"

pqerrors is a error mapper for postgres.

Dedicated error mapper for go postgres driver (lib/pq.Error) package is included under the path of github.com/atlas-app-toolkit/errors/mappers/pqerrors. This package includes following components:

  • Condition function CondPQ, CondConstraintEq, CondCodeEq for conditions involved in *pq.Error detection, specific constraint name and specific status code of postgres error respectively.

  • ToMapFunc function that converts mapping function that deals with pq.Error to avoid burden of casting errors back and forth.

  • Default mapping function that can be included into errors interceptor for FK contraints (NewForeignKeyMapping), RESTRICT (NewRestrictMapping), NOT NULL (NewNotNullMapping), PK/UNIQUE (NewUniqueMapping)

Example Usage:

import (
	...
	"github.com/atlas-app-toolkit/errors/mappers/pqerrors"
)

interceptor := errors.UnaryServerInterceptor(
	...
	pqerrors.NewUniqueMapping("emails_address_key", "Contacts", "Primary Email Address"),
	...
)

Any violation of UNIQUE constraint "email_address_key" will result in following error:

{
  "error": {
    "status": 409,
    "code": "ALREADY_EXISTS",
    "message": "There is already an existing 'Contacts' object with the same 'Primary Email Address'."
  }
}

Documentation

Overview

The Error Container entity serves a purpose for keeping track of errors, details and fields information. This component can be used explicitly (for example when we want to sequentially fill it with details and fields), as well as implicitly (all errors that are returned from handler are transformed to an Error Container, and passed as GRPCStatus to a gRPC Gateway).

Index

Constants

View Source
const (
	// Context key for Error Container.
	DefaultErrorContainerKey = "Error-Container"
)

Variables

This section is empty.

Functions

func Error

func Error(ctx context.Context) error

Error function returns an error container if any error field, detail or message was set, else it returns nil. Use New to define and return error message in place.

func IfSet

func IfSet(ctx context.Context, code codes.Code, format string, args ...interface{}) error

IfSet function intializes general error code and error message for context stored error container if and onyl if any error was set previously by calling Set, WithField(s), WithDetails(s).

func Map

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

Map function performs mapping based on context stored error container's mapping configuration.

func NewContext

func NewContext(ctx context.Context, c *Container) context.Context

NewContext function creates a context with error container saved in it.

func UnaryServerInterceptor

func UnaryServerInterceptor(mapFuncs ...MapFunc) grpc.UnaryServerInterceptor

UnaryServerInterceptor returns grpc.UnaryServerInterceptor that should be used as a middleware to generate Error Messages with Details and Field Information with Mapping given.

Types

type Container

type Container struct {

	// Mapper structure performs necessary mappings.
	Mapper
	// contains filtered or unexported fields
}

Container struct is an entity that servers a purpose of error container and consist of methods to append details, field errors and setting general error code/message.

func Detail

func Detail(ctx context.Context, code codes.Code, target string, format string, args ...interface{}) *Container

Detail function appends a new detail to a context stored error container's 'details' section.

func Details

func Details(ctx context.Context, details ...*errdetails.TargetInfo) *Container

Details function appends a list of details to a context stored error container's 'details' section.

func Field

func Field(ctx context.Context, target string, format string, args ...interface{}) *Container

Field function appends a field error detail to a context stored error container's 'fields' section.

func Fields

func Fields(ctx context.Context, fields map[string][]string) *Container

Fields function appends a multiple fields error details to a context stored error container's 'fields' section.

func FromContext

func FromContext(ctx context.Context) *Container

FromContext function retrieves an error container value from context.

func InitContainer

func InitContainer() *Container

func New

func New(ctx context.Context, code codes.Code, format string, args ...interface{}) *Container

New function resets any error that was inside context stored error container and replaces it with a new error.

func NewContainer

func NewContainer(code codes.Code, format string, args ...interface{}) *Container

NewContainer function returns a new entity of error container.

func Set

func Set(ctx context.Context, target string, code codes.Code, format string, args ...interface{}) *Container

Set function initializes a general error code and error message for context stored error container and also appends a details with the same content to an error container's 'details' section.

func (Container) Error

func (c Container) Error() string

Error function returns error message currently associated with container.

func (*Container) GRPCStatus

func (c *Container) GRPCStatus() *status.Status

GRPCStatus function returns an error container as GRPC status.

func (*Container) IfSet

func (c *Container) IfSet(code codes.Code, format string, args ...interface{}) error

IfSet function initializes general error code and error message for error container if and only if any error was set previously by calling Set, WithField(s), WithDetail(s).

func (*Container) IsSet

func (c *Container) IsSet() bool

IsSet function returns flag that determines whether the main error code and error message were set or not.

func (*Container) New

func (c *Container) New(code codes.Code, format string, args ...interface{}) *Container

New function instantinates general error code and error message for error container.

func (*Container) Set

func (c *Container) Set(target string, code codes.Code, format string, args ...interface{}) *Container

Set function initializes general error code and error message for error container and also appends a detail with the same content to a an error container's 'details' section.

func (*Container) WithDetail

func (c *Container) WithDetail(code codes.Code, target string, format string, args ...interface{}) *Container

WithDetail function appends a new Detail to an error container's 'details' section.

func (*Container) WithDetails

func (c *Container) WithDetails(details ...*errdetails.TargetInfo) *Container

WithDetails function appends a list of error details to an error container's 'details' section.

func (*Container) WithField

func (c *Container) WithField(target string, format string, args ...interface{}) *Container

WithField function appends a field error detail to an error container's 'fields' section.

func (*Container) WithFields

func (c *Container) WithFields(fields map[string][]string) *Container

WithFields function appends a several fields error details to an error container's 'fields' section.

type MapCond

type MapCond func(error) bool

MapCond function takes an error and returns flag that indicates whether the map condition was met.

func CondAnd

func CondAnd(mcs ...MapCond) MapCond

CondAnd function takes a list of condition function as an input and returns a function that asserts true if and only if all conditions are satisfied.

func CondEq

func CondEq(src string) MapCond

CondEq function takes a string as an input and returns a condition function that checks whether the error is equal to a string given.

func CondHasPrefix

func CondHasPrefix(prefix string) MapCond

CondHasPrefix function takes a string as an input and returns a condition function that checks whether the error starts with the string given.

func CondHasSuffix

func CondHasSuffix(suffix string) MapCond

CondHasSuffix function takes a string as an input and returns a condition function that checks whether the error ends with the string given.

func CondNot

func CondNot(mc MapCond) MapCond

CondNot function takes a condtion function as an input and returns a function that asserts inverse result.

func CondOr

func CondOr(mcs ...MapCond) MapCond

CondOr function takes a list of condition function as an input and returns a function that asserts true if at least one of conditions is satisfied.

func CondReMatch

func CondReMatch(pattern string) MapCond

CondReMatch function takes a string regexp pattern as an input and returns a condition function that checks whether the error matches the pattern given.

func (MapCond) Error

func (mc MapCond) Error() string

Error function ...

type MapFunc

type MapFunc func(context.Context, error) (error, bool)

MapFunc function takes an error and returns mapped error and flag that indicates whether the mapping was performed successfully.

func NewMapping

func NewMapping(src error, dst error) MapFunc

NewMapping function creates a mapping function based on error interfaces passed to it. src can be either MapCond and dst can be MapFunc.

func (MapFunc) Error

func (mc MapFunc) Error() string

Error function ...

type Mapper

type Mapper struct {
	// contains filtered or unexported fields
}

Mapper struct ...

func (*Mapper) AddMapping

func (m *Mapper) AddMapping(mf ...MapFunc) *Mapper

AddMapping function appends a list of mapping functions to a mapping chain.

func (*Mapper) Map

func (m *Mapper) Map(ctx context.Context, err error) error

Map function performs a mapping from error given following a chain of mappings that were defined prior to the Map call.

Directories

Path Synopsis
mappers

Jump to

Keyboard shortcuts

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