Skip to content

Commit

Permalink
Merge pull request #2 from ing-bank/v2
Browse files Browse the repository at this point in the history
v2 concept
  • Loading branch information
survivorbat authored Aug 9, 2023
2 parents 47fa321 + 9d74633 commit a97a032
Show file tree
Hide file tree
Showing 10 changed files with 751 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
![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.
Expand Down
15 changes: 15 additions & 0 deletions v2/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.idea/
.vscode/

# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out
21 changes: 21 additions & 0 deletions v2/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2022 ING

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
15 changes: 15 additions & 0 deletions v2/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
MAKEFLAGS := --no-print-directory --silent

default: help

help:
@echo "Please use 'make <target>' where <target> is one of"
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z\._-]+:.*?## / {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)

t: test
test: fmt ## Run unit tests, alias: t
go test ./... -timeout=30s -parallel=8

fmt: ## Format go code
@go mod tidy
@go fmt ./...
84 changes: 84 additions & 0 deletions v2/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# 🦁 Gin Error Registry

[![Go package](https://github.com/ing-bank/ginerr/actions/workflows/test.yaml/badge.svg)](https://github.com/ing-bank/ginerr/actions/workflows/test.yaml)
![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)

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

## 👷 V2 migration guide

V2 of this library changes the interface of all the methods to allow contexts to be passed to handlers. This
allows you to add additional data to the final response.

The interface changes are as follows.

- `RegisterErrorHandler` and all its variants take a context as a first parameter in the handler, allowing you to pass more data to the response
- `RegisterErrorHandler` and all its variants require the callback function to return `(int, any)` instead of `(int, R)`, removing the unnecessary generic
- Both `NewErrorResponse` and `NewErrorResponseFrom` take a context as a first parameter, this could be the request context but that's up to you

## ⬇️ Installation

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

## 📋 Usage

```go
package main

import (
"github.com/gin-gonic/gin"
"github.com/ing-bank/ginerr/v2"
"net/http"
)

type MyError struct {
}

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

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

func main() {
handler := func(ctx context.Context, myError *MyError) (int, any) {
return http.StatusInternalServerError, Response{
Errors: map[string]any{
"error": myError.Error(),
},
}
}

ginerr.RegisterErrorHandler(handler)

// [...]
}

func handleGet(c *gin.Context) {
err := &MyError{}
c.JSON(ginerr.NewErrorResponse(c.Request.Context(), err))
}
```

## 🚀 Development

1. Clone the repository
2. Run `make t` to run unit tests
3. Run `make fmt` to format code

You can run `make` to see a list of useful commands.

## 🔭 Future Plans

Nothing here yet!
123 changes: 123 additions & 0 deletions v2/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package ginerr

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

const defaultCode = http.StatusInternalServerError

// DefaultErrorRegistry is the default ErrorRegistry for the application, can be overridden for rare use-cases.
var DefaultErrorRegistry = NewErrorRegistry()

type internalHandler func(ctx context.Context, err error) (int, any)
type internalStringHandler func(ctx context.Context, err string) (int, any)

// NewErrorRegistry is ideal for testing or overriding the default one.
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(ctx context.Context, err error) (int, any) {
// Check if the error string exists
if handler, ok := registry.stringHandlers[err.Error()]; ok {
return handler(ctx, err.Error())
}

return registry.DefaultCode, registry.DefaultResponse
}

return registry
}

// ErrorRegistry contains a map of ErrorHandlers.
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

// DefaultResponse to return when no handler is found
DefaultResponse any
}

func (e *ErrorRegistry) SetDefaultResponse(code int, response any) {
e.DefaultCode = code
e.DefaultResponse = response
}

// NewErrorResponse Returns an error response using the DefaultErrorRegistry. If no specific handler could be found,
// it will return the defaults.
func NewErrorResponse(ctx context.Context, err error) (int, any) {
return NewErrorResponseFrom(DefaultErrorRegistry, ctx, err)
}

// NewErrorResponseFrom Returns an error response using the given registry. If no specific handler could be found,
// it will return the defaults.
func NewErrorResponseFrom(registry *ErrorRegistry, ctx context.Context, err error) (int, any) {
errorType := fmt.Sprintf("%T", err)

// If a handler is registered for the error type, use it.
if entry, ok := registry.handlers[errorType]; ok {
return entry(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
}

// RegisterErrorHandler registers an error handler in DefaultErrorRegistry. The R type is the type of the response body.
func RegisterErrorHandler[E error](handler func(context.Context, E) (int, any)) {
RegisterErrorHandlerOn(DefaultErrorRegistry, handler)
}

// RegisterErrorHandlerOn registers an error handler in the given registry. The R type is the type of the response body.
func RegisterErrorHandlerOn[E error](registry *ErrorRegistry, handler func(context.Context, E) (int, any)) {
// Name of the type
errorType := fmt.Sprintf("%T", *new(E))

// 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(ctx context.Context, err error) (int, any) {
return handler(ctx, err.(E))
}
}

// 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.
func RegisterCustomErrorTypeHandler(errorType string, handler func(ctx context.Context, err error) (int, any)) {
RegisterCustomErrorTypeHandlerOn(DefaultErrorRegistry, errorType, handler)
}

// 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.
func RegisterCustomErrorTypeHandlerOn(registry *ErrorRegistry, errorType string, handler func(ctx context.Context, err error) (int, any)) {
// Wrap it in a closure, we can't save it directly
registry.handlers[errorType] = handler
}

// 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.
func RegisterStringErrorHandler(errorString string, handler func(ctx context.Context, err string) (int, any)) {
RegisterStringErrorHandlerOn(DefaultErrorRegistry, errorString, handler)
}

// 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.
func RegisterStringErrorHandlerOn(registry *ErrorRegistry, errorString string, handler func(ctx context.Context, err string) (int, any)) {
registry.stringHandlers[errorString] = handler
}
Loading

0 comments on commit a97a032

Please sign in to comment.