Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: ing-bank/ginerr
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.0.2
Choose a base ref
...
head repository: ing-bank/ginerr
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref
  • 9 commits
  • 17 files changed
  • 2 contributors

Commits on Jun 5, 2024

  1. Delete v1

    survivorbat committed Jun 5, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    2df0291 View commit details
  2. Copy the full SHA
    bdeb163 View commit details

Commits on Nov 25, 2024

  1. Initial version of v3

    survivorbat committed Nov 25, 2024
    Copy the full SHA
    2fe9e66 View commit details
  2. Simplify version matrices

    survivorbat committed Nov 25, 2024
    Copy the full SHA
    db8fb93 View commit details
  3. Fix comment

    survivorbat committed Nov 25, 2024
    Copy the full SHA
    1592352 View commit details

Commits on Nov 28, 2024

  1. Copy the full SHA
    242ebd5 View commit details
  2. Merge pull request #8 from ing-bank/v3

    Initial version of v3
    survivorbat authored Nov 28, 2024
    Copy the full SHA
    a773aa4 View commit details

Commits on Jan 8, 2025

  1. Copy the full SHA
    0eef28e View commit details
  2. Copy the full SHA
    76665a7 View commit details
Showing with 334 additions and 1,358 deletions.
  1. +1 −1 .github/workflows/test.yaml
  2. +30 −85 .golangci.yaml
  3. +3 −2 Makefile
  4. +18 −49 README.md
  5. +74 −115 errors.go
  6. +148 −200 errors_test.go
  7. +58 −43 examples_test.go
  8. +2 −2 go.mod
  9. +0 −15 v2/.gitignore
  10. +0 −21 v2/LICENSE
  11. +0 −22 v2/Makefile
  12. +0 −86 v2/README.md
  13. +0 −159 v2/errors.go
  14. +0 −443 v2/errors_test.go
  15. +0 −68 v2/examples_test.go
  16. +0 −14 v2/go.mod
  17. +0 −33 v2/go.sum
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: [ '1.18', '1.19', '1.20' ]
go-version: [ '1.23.0' ]
steps:
- uses: actions/checkout@v3

115 changes: 30 additions & 85 deletions .golangci.yaml
Original file line number Diff line number Diff line change
@@ -1,96 +1,41 @@
issues:
exclude-rules:
- path: (.+)_test.go
linters:
- goconst # Test data doesn't need to be in constants
- err113 # Necessary for tests
- fatcontext # We save contexts for validation

linters-settings:
nlreturn:
block-size: 3

gocritic:
disabled-checks:
- "paramTypeCombine"
- "unnamedResult"
enabled-tags:
- "performance"
- "style"
- "diagnostic"

issues:
exclude-rules:
- path: (.+)_test.go
linters:
- funlen
- goconst
- nilnil
- goerr113
govet:
enable-all: true
disable:
- fieldalignment

linters:
enable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- containedctx
- contextcheck
- cyclop
- decorder
- dogsled
- dupword
- durationcheck
- errcheck
- errname
- errorlint
- execinquery
- exhaustive
- exportloopref
- forbidigo
- ginkgolinter
- gocheckcompilerdirectives
- gochecknoinits
- gocognit
- goconst
- gocritic
- gocyclo
- goerr113
- gofmt
- gofumpt
- goheader
- goimports
- gomoddirectives
- gomodguard
- goprintffuncname
- gosec
- gosimple
- gosmopolitan
- govet
- grouper
- ifshort
- importas
- ineffassign
- interfacebloat
- interfacer
- ireturn
- loggercheck
- maintidx
- makezero
- mirror
- misspell
- musttag
- nakedret
- nestif
- nilerr
- nilnil
- nlreturn
- nonamedreturns
- nosprintfhostport
- paralleltest
- prealloc
- predeclared
- promlinter
- reassign
- rowserrcheck
- sqlclosecheck
- staticcheck
- tenv
- thelper
- tparallel
- typecheck
- unconvert
- unparam
- unused
- usestdlibvars
- wastedassign
- whitespace
- zerologlint
enable-all: true
disable:
# Disabled because they're too strict
- gochecknoglobals # We sometimes use global variables for ease of use
- depguard # Unnecessary check
- exhaustruct # We don't always have to fill all fields
- lll # We don't enforce line lengths
- wsl # Too strict about statement placement
- wrapcheck # We don't enforce wrapping
- varnamelen # We don't enforce var name length
- testpackage # We don't use test packages
- funlen # We don't enforce the length of a function
- exportloopref # Deprecated and no longer required in 1.22

5 changes: 3 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -11,8 +11,9 @@ test: fmt ## Run unit tests, alias: t
go test ./... -timeout=30s -parallel=8

fmt: ## Format go code
@go mod tidy
@gofumpt -l -w .
go mod tidy
gofumpt -l -w .
golangci-lint run --fix ./...

tools: ## Install extra tools for development
go install mvdan.cc/gofumpt@latest
67 changes: 18 additions & 49 deletions README.md
Original file line number Diff line number Diff line change
@@ -4,70 +4,39 @@
![GitHub](https://img.shields.io/github/license/ing-bank/ginerr)
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/ing-bank/ginerr)

**[❗ 🚨 Click here for version 2 🚨 ❗](./v2)**

Sending any error back to the user can pose a [big security risk](https://owasp.org/www-community/Improper_Error_Handling).
For this reason we developed an error registry that allows you to register specific error handlers
for your application. This way you can control what information is sent back to the user.

You can register errors in 3 ways:
- By error type
- By value of string errors
- By defining the error name yourself

## ⬇️ Installation

`go get github.com/ing-bank/ginerr`

## 📋 Usage

```go
package main

import (
"github.com/gin-gonic/gin"
"github.com/ing-bank/ginerr"
"net/http"
)
## 👷 V3 migration guide

type MyError struct {
}
V3 completely revamps the `ErrorRegistry` and now utilises the `errors` package to match errors.
The following changes have been made:

func (m *MyError) Error() string {
return "Something went wrong!"
}
- `RegisterErrorHandler` now requires a concrete instance of the error as its first argument
- `RegisterErrorHandlerOn` now requires a concrete instance of the error as its second argument
- `RegisterStringErrorHandler` has been removed, use static `errors.New` in `RegisterErrorHandler` to get this to work
- `RegisterStringErrorHandlerOn` has been removed, use static `errors.New` in `RegisterErrorHandlerOn` to get this to work
- `RegisterCustomErrorTypeHandler` has been removed, wrap unexported errors from libraries to create handlers for these
- `RegisterCustomErrorTypeHandlerOn` has been removed, wrap unexported errors from libraries to create handlers for these
- `ErrorRegistry` changes:
- `DefaultCode` has been removed, use `RegisterDefaultHandler` instead
- `DefaultResponse` has been removed, use `RegisterDefaultHandler` instead
- `SetDefaultResponse` has been removed, use `RegisterDefaultHandler` instead

// Response is an example response object, you can return anything you like
type Response struct {
Errors map[string]any `json:"errors,omitempty"`
}
## ⬇️ Installation

func main() {
handler := func(myError *MyError) (int, Response) {
return http.StatusInternalServerError, Response{
Errors: map[string]any{
"error": myError.Error(),
},
}
}
`go get github.com/ing-bank/ginerr/v3`

ginerr.RegisterErrorHandler(handler)

// [...]
}
## 📋 Usage

func handleGet(c *gin.Context) {
err := &MyError{}
c.JSON(ginerr.NewErrorResponse(err))
}
```
Check out [the examples here](./examples_test.go).

## 🚀 Development

1. Clone the repository
2. Run `make tools` to install necessary tools
3. Run `make t` to run unit tests
4. Run `make fmt` to format code
3. Run `make fmt` to format code
4. Run `make lint` to lint your code

You can run `make` to see a list of useful commands.
189 changes: 74 additions & 115 deletions errors.go
Original file line number Diff line number Diff line change
@@ -1,160 +1,119 @@
package ginerr

import (
"context"
"errors"
"fmt"
"net/http"
)

const defaultCode = http.StatusInternalServerError

// Deprecated: Please use v2 of this library
// DefaultErrorRegistry is a global singleton empty ErrorRegistry for convenience.
var DefaultErrorRegistry = NewErrorRegistry()

type (
internalHandler func(err error) (int, any)
internalStringHandler func(err string) (int, any)
)

// CustomErrorHandler is the template for unexported errors. For example binding.SliceValidationError
// or uuid.invalidLengthError
// Deprecated: Please use v2 of this library
type CustomErrorHandler[R any] func(err error) (int, R)
// errorHandler encompasses the methods necessary to validate an error and calculate a response.
type errorHandler struct {
// isStringError is used to catch cases where errors.New() is being used as error types,
// as we need to use errors.Is for those cases, errors.As is not enough
isStringError bool

// ErrorStringHandler is the template for string errors that don't have their own object available. For example
// "record not found" or "invalid input"
// Deprecated: Please use v2 of this library
type ErrorStringHandler[R any] func(err string) (int, R)
// isType is a wrapped around an `errors.As` from RegisterERrorHandler with the type information
// of the target error still intact.
isType func(err error) bool

// ErrorHandler is the template of an error handler in the ErrorRegistry. The E type is the error type that
// the handler is registered for. The R type is the type of the response body.
// Deprecated: Please use v2 of this library
type ErrorHandler[E error, R any] func(E) (int, R)
// handle will calculate the response. It's a wrapper around the user-provided handler
// which ensures that the type of the error is properly asserted using `errors.As`.
handle func(ctx context.Context, err error) (int, any)
}

// Deprecated: Please use v2 of this library
// NewErrorRegistry instantiates a new ErrorRegistry. If you're looking for the 'default' error
// registry, check out DefaultErrorRegistry.
func NewErrorRegistry() *ErrorRegistry {
registry := &ErrorRegistry{
handlers: make(map[string]internalHandler),
stringHandlers: make(map[string]internalStringHandler),
DefaultCode: defaultCode,
}

// Make sure the stringHandlers are available in the handlers
registry.handlers["*errors.errorString"] = func(err error) (int, any) {
// Check if the error string exists
if handler, ok := registry.stringHandlers[err.Error()]; ok {
return handler(err.Error())
}

return registry.DefaultCode, registry.DefaultResponse
handlers: make(map[error]*errorHandler),
defaultHandler: func(context.Context, error) (int, any) {
return http.StatusInternalServerError, nil
},
}

return registry
}

// Deprecated: Please use v2 of this library
// ErrorRegistry is the place where errors and callbacks are stored.
type ErrorRegistry struct {
// handlers are used when we know the type of the error
handlers map[string]internalHandler

// stringHandlers are used when the error is only a string
stringHandlers map[string]internalStringHandler

// DefaultCode to return when no handler is found
DefaultCode int
// handlers maps error types with their handlers
handlers map[error]*errorHandler

// DefaultResponse to return when no handler is found
DefaultResponse any
// defaultHandler is called if no matching error was registered
defaultHandler func(ctx context.Context, err error) (int, any)
}

// Deprecated: Please use v2 of this library
func (e *ErrorRegistry) SetDefaultResponse(code int, response any) {
e.DefaultCode = code
e.DefaultResponse = response
func (e *ErrorRegistry) RegisterDefaultHandler(callback func(ctx context.Context, err error) (int, any)) {
e.defaultHandler = callback
}

// NewErrorResponse Returns an error response using the DefaultErrorRegistry. If no specific handler could be found,
// it will return the defaults. It returns an HTTP status code and a response object.
//
// Deprecated: Please use v2 of this library
//
//nolint:gocritic // Unnamed return arguments are described
func NewErrorResponse(err error) (int, any) {
return NewErrorResponseFrom(DefaultErrorRegistry, err)
// it will return the defaults.
func NewErrorResponse(ctx context.Context, err error) (int, any) {
return NewErrorResponseFrom(ctx, DefaultErrorRegistry, err)
}

// NewErrorResponseFrom Returns an error response using the given registry. If no specific handler could be found,
// it will return the defaults. It returns an HTTP status code and a response object.
//
// Deprecated: Please use v2 of this library
//
//nolint:gocritic // Unnamed return arguments are described
func NewErrorResponseFrom(registry *ErrorRegistry, err error) (int, any) {
errorType := fmt.Sprintf("%T", err)
// it will return the defaults.
func NewErrorResponseFrom[E error](ctx context.Context, registry *ErrorRegistry, err E) (int, any) {
for errConcrete, handler := range registry.handlers {
// We can't use `errors.As` here directly, as we don't have a concrete version of the type here
if !handler.isType(err) {
continue
}

// If it's a string error, it must match the given error exactly, otherwise it might mix up if we only
// check on type
if handler.isStringError {
if errors.Is(err, errConcrete) {
// It might be wrapped, so we pass the concrete type
return handler.handle(ctx, errConcrete)
}

// If a handler is registered for the error type, use it.
if entry, ok := registry.handlers[errorType]; ok {
return entry(err)
continue
}

return handler.handle(ctx, err)
}

// In production, we should return a generic error message. If you want to know why, read this:
// https://owasp.org/www-community/Improper_Error_Handling
return registry.DefaultCode, registry.DefaultResponse
return registry.defaultHandler(ctx, err)
}

// RegisterErrorHandler registers an error handler in DefaultErrorRegistry. The R type is the type of the response body.
// Deprecated: Please use v2 of this library
func RegisterErrorHandler[E error, R any](handler ErrorHandler[E, R]) {
RegisterErrorHandlerOn(DefaultErrorRegistry, handler)
// RegisterErrorHandler registers an error handler in DefaultErrorRegistry.
func RegisterErrorHandler[E error](instance E, handler func(context.Context, E) (int, any)) {
RegisterErrorHandlerOn(DefaultErrorRegistry, instance, handler)
}

// RegisterErrorHandlerOn registers an error handler in the given registry. The R type is the type of the response body.
// Deprecated: Please use v2 of this library
func RegisterErrorHandlerOn[E error, R any](registry *ErrorRegistry, handler ErrorHandler[E, R]) {
// Name of the type
errorType := fmt.Sprintf("%T", *new(E))
// errorStringType is used to check if an error was created by errors.New or fmt.Errorf
//
//nolint:err113 // We need it here for the type name
var errorStringType = fmt.Sprintf("%T", errors.New(""))

// RegisterErrorHandlerOn registers an error handler in the given registry.
func RegisterErrorHandlerOn[E error](registry *ErrorRegistry, instance E, handler func(context.Context, E) (int, any)) {
// Wrap it in a closure, we can't save it directly because err E is not available in NewErrorResponseFrom. It will
// be available in the closure when it is called. Check out TestErrorResponseFrom_ReturnsErrorBInInterface for an example.
registry.handlers[errorType] = func(err error) (int, any) {
// We can safely cast it here, because we know it's the right type.
//nolint:errorlint // Not relevant, we're casting anyway
return handler(err.(E))
}
}
registry.handlers[instance] = &errorHandler{
// Necessary to make sure we match error strings using `errors.Is`
isStringError: fmt.Sprintf("%T", instance) == errorStringType,

// RegisterCustomErrorTypeHandler registers an error handler in DefaultErrorRegistry. Same as RegisterErrorHandler,
// but you can set the fmt.Sprint("%T", err) error yourself. Allows you to register error types that aren't exported
// from their respective packages such as the uuid error or *errors.errorString. The R type is the type of the response body.
// Deprecated: Please use v2 of this library
func RegisterCustomErrorTypeHandler[R any](errorType string, handler CustomErrorHandler[R]) {
RegisterCustomErrorTypeHandlerOn(DefaultErrorRegistry, errorType, handler)
}
// Handler that uses errors.As to cast to an error
handle: func(ctx context.Context, err error) (int, any) {
var errorOfType E

// RegisterCustomErrorTypeHandlerOn registers an error handler in the given registry. Same as RegisterErrorHandlerOn,
// but you can set the fmt.Sprint("%T", err) error yourself. Allows you to register error types that aren't exported
// from their respective packages such as the uuid error or *errors.errorString. The R type is the type of the response body.
// Deprecated: Please use v2 of this library
func RegisterCustomErrorTypeHandlerOn[R any](registry *ErrorRegistry, errorType string, handler CustomErrorHandler[R]) {
// Wrap it in a closure, we can't save it directly
registry.handlers[errorType] = func(err error) (int, any) {
return handler(err)
}
}
// This function should only be called if errors.Is succeeded, so this should never fail
_ = errors.As(err, &errorOfType)

// RegisterStringErrorHandler allows you to register an error handler for a simple errorString created with
// errors.New() or fmt.Errorf(). Can be used in case you are dealing with libraries that don't have exported
// error objects. Uses the DefaultErrorRegistry. The R type is the type of the response body.
// Deprecated: Please use v2 of this library
func RegisterStringErrorHandler[R any](errorString string, handler ErrorStringHandler[R]) {
RegisterStringErrorHandlerOn(DefaultErrorRegistry, errorString, handler)
}
return handler(ctx, errorOfType)
},

// RegisterStringErrorHandlerOn allows you to register an error handler for a simple errorString created with
// errors.New() or fmt.Errorf(). Can be used in case you are dealing with libraries that don't have exported
// error objects. The R type is the type of the response body.
// Deprecated: Please use v2 of this library
func RegisterStringErrorHandlerOn[R any](registry *ErrorRegistry, errorString string, handler ErrorStringHandler[R]) {
registry.stringHandlers[errorString] = func(err string) (int, any) {
return handler(err)
// Type check, as we need `instance` from this function
isType: func(err error) bool {
return errors.As(err, &instance)
},
}
}
348 changes: 148 additions & 200 deletions errors_test.go

Large diffs are not rendered by default.

101 changes: 58 additions & 43 deletions examples_test.go
Original file line number Diff line number Diff line change
@@ -1,65 +1,80 @@
package ginerr

import "net/http"
import (
"context"
"errors"
"fmt"
"net/http"
)

type Response struct {
Errors map[string]any `json:"errors,omitempty"`
}
var ErrDatabaseOverloaded = errors.New("database overloaded")

type MyError struct{}
type InputValidationError struct {
// ...
}

func (m MyError) Error() string {
return "Something went wrong!"
func (m *InputValidationError) Error() string {
return "..."
}

func ExampleRegisterErrorHandler() {
handler := func(myError *MyError) (int, any) {
return http.StatusInternalServerError, Response{
Errors: map[string]any{
"error": myError.Error(),
},
}
// Write your error handlers
validationHandler := func(_ context.Context, err *InputValidationError) (int, any) {
return http.StatusBadRequest, "Your input was invalid: " + err.Error()
}
databaseOverloadedHandler := func(context.Context, error) (int, any) {
return http.StatusBadGateway, "Please try again later"
}

// Register the error handlers and supply an empty version of the type for type reference
RegisterErrorHandler(&InputValidationError{}, validationHandler)
RegisterErrorHandler(ErrDatabaseOverloaded, databaseOverloadedHandler)

// Return errors somewhere deep in your code
errA := fmt.Errorf("validation error: %w", &InputValidationError{})
errB := fmt.Errorf("could not connect to database: %w", ErrDatabaseOverloaded)

// In your HTTP handlers, instantiate responses and return those to the users
codeA, responseA := NewErrorResponse(context.Background(), errA)
codeB, responseB := NewErrorResponse(context.Background(), errB)

// Check the output
fmt.Printf("%d: %s\n", codeA, responseA)
fmt.Printf("%d: %s\n", codeB, responseB)

RegisterErrorHandler(handler)
// Output:
// 400: Your input was invalid: ...
// 502: Please try again later
}

func ExampleRegisterErrorHandlerOn() {
registry := NewErrorRegistry()

handler := func(myError *MyError) (int, any) {
return http.StatusInternalServerError, Response{
Errors: map[string]any{
"error": myError.Error(),
},
}
// Write your error handlers
validationHandler := func(_ context.Context, err *InputValidationError) (int, any) {
return http.StatusBadRequest, "Your input was invalid: " + err.Error()
}

RegisterErrorHandlerOn(registry, handler)
}

func ExampleRegisterStringErrorHandler() {
handler := func(myError string) (int, any) {
return http.StatusInternalServerError, Response{
Errors: map[string]any{
"error": myError,
},
}
databaseOverloadedHandler := func(context.Context, error) (int, any) {
return http.StatusBadGateway, "please try again later"
}

RegisterStringErrorHandler("some string error", handler)
}
// Register the error handlers and supply an empty version of the type for type reference
RegisterErrorHandlerOn(registry, &InputValidationError{}, validationHandler)
RegisterErrorHandlerOn(registry, ErrDatabaseOverloaded, databaseOverloadedHandler)

func ExampleRegisterStringErrorHandlerOn() {
registry := NewErrorRegistry()
// Return errors somewhere deep in your code
errA := fmt.Errorf("validation error: %w", &InputValidationError{})
errB := fmt.Errorf("could not connect to database: %w", ErrDatabaseOverloaded)

handler := func(myError string) (int, any) {
return http.StatusInternalServerError, Response{
Errors: map[string]any{
"error": myError,
},
}
}
// In your HTTP handlers, instantiate responses and return those to the users
codeA, responseA := NewErrorResponseFrom(context.Background(), registry, errA)
codeB, responseB := NewErrorResponseFrom(context.Background(), registry, errB)

// Check the output
fmt.Printf("%d: %s\n", codeA, responseA)
fmt.Printf("%d: %s\n", codeB, responseB)

RegisterStringErrorHandlerOn(registry, "some string error", handler)
// Output:
// 400: Your input was invalid: ...
// 502: please try again later
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/ing-bank/ginerr
module github.com/ing-bank/ginerr/v3

go 1.20
go 1.23.0

require github.com/stretchr/testify v1.8.1

15 changes: 0 additions & 15 deletions v2/.gitignore

This file was deleted.

21 changes: 0 additions & 21 deletions v2/LICENSE

This file was deleted.

22 changes: 0 additions & 22 deletions v2/Makefile

This file was deleted.

86 changes: 0 additions & 86 deletions v2/README.md

This file was deleted.

159 changes: 0 additions & 159 deletions v2/errors.go

This file was deleted.

443 changes: 0 additions & 443 deletions v2/errors_test.go

This file was deleted.

68 changes: 0 additions & 68 deletions v2/examples_test.go

This file was deleted.

14 changes: 0 additions & 14 deletions v2/go.mod

This file was deleted.

33 changes: 0 additions & 33 deletions v2/go.sum

This file was deleted.