Skip to content

Commit

Permalink
feat: implement error handling utilities package (#194)
Browse files Browse the repository at this point in the history
  • Loading branch information
paul-nicolas authored and flemzord committed May 12, 2023
1 parent a418c19 commit e821b75
Show file tree
Hide file tree
Showing 38 changed files with 707 additions and 597 deletions.
65 changes: 37 additions & 28 deletions pkg/api/apierrors/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,40 @@ import (
"strings"

"github.com/formancehq/ledger/pkg/ledger"
"github.com/formancehq/ledger/pkg/ledger/runner"
"github.com/formancehq/ledger/pkg/ledger/state"
"github.com/formancehq/ledger/pkg/machine/vm"
"github.com/formancehq/ledger/pkg/storage"
"github.com/formancehq/stack/libs/go-libs/api"
"github.com/formancehq/stack/libs/go-libs/logging"
"github.com/pkg/errors"
)

const (
ErrInternal = "INTERNAL"
ErrConflict = "CONFLICT"
ErrInsufficientFund = "INSUFFICIENT_FUND"
ErrValidation = "VALIDATION"
ErrContextCancelled = "CONTEXT_CANCELLED"
ErrStore = "STORE"
ErrNotFound = "NOT_FOUND"
ErrScriptCompilationFailed = "COMPILATION_FAILED"
ErrScriptNoScript = "NO_SCRIPT"
ErrScriptMetadataOverride = "METADATA_OVERRIDE"
ErrInternal = "INTERNAL"
ErrConflict = "CONFLICT"
ErrInsufficientFund = "INSUFFICIENT_FUND"
ErrValidation = "VALIDATION"
ErrContextCancelled = "CONTEXT_CANCELLED"
ErrStore = "STORE"
ErrNotFound = "NOT_FOUND"
ErrScriptCompilationFailed = "COMPILATION_FAILED"
ErrScriptNoScript = "NO_SCRIPT"
ErrScriptMetadataOverride = "METADATA_OVERRIDE"
ScriptErrorInsufficientFund = "INSUFFICIENT_FUND"
ScriptErrorCompilationFailed = "COMPILATION_FAILED"
ScriptErrorNoScript = "NO_SCRIPT"
ScriptErrorMetadataOverride = "METADATA_OVERRIDE"
ResourceResolutionError = "RESOURCE_RESOLUTION_ERROR"
)

func ResponseError(w http.ResponseWriter, r *http.Request, err error) {
status, code, details := coreErrorToErrorCode(err)

baseError := errors.Cause(err)

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if status < 500 {
err := json.NewEncoder(w).Encode(api.ErrorResponse{
ErrorCode: code,
ErrorMessage: err.Error(),
ErrorMessage: baseError.Error(),
Details: details,
})
if err != nil {
Expand All @@ -52,27 +55,33 @@ func ResponseError(w http.ResponseWriter, r *http.Request, err error) {

func coreErrorToErrorCode(err error) (int, string, string) {
switch {
case state.IsConflictError(err):
case ledger.IsConflictError(err):
return http.StatusConflict, ErrConflict, ""
case
ledger.IsValidationError(err),
state.IsPastTransaction(err),
runner.IsNoPostingsError(err):
ledger.IsPastTransactionError(err),
ledger.IsNoPostingsError(err):
return http.StatusBadRequest, ErrValidation, ""
case ledger.IsNotFoundError(err):
return http.StatusNotFound, ErrNotFound, ""
// TODO(gfyrag): Those error codes are copied from vm package. We need to clean this
case vm.IsScriptErrorWithCode(err, ErrScriptNoScript),
vm.IsScriptErrorWithCode(err, ErrInsufficientFund),
vm.IsScriptErrorWithCode(err, ErrScriptCompilationFailed),
vm.IsScriptErrorWithCode(err, ErrScriptMetadataOverride),
vm.IsResourceResolutionError(err):
scriptErr := &vm.ScriptError{}
_ = errors.As(err, &scriptErr)
return http.StatusBadRequest, scriptErr.Code, EncodeLink(scriptErr.Message)
case ledger.IsNoScriptError(err):
baseError := errors.Cause(err)
return http.StatusBadRequest, ScriptErrorNoScript, EncodeLink(baseError.Error())
case ledger.IsInsufficientFundError(err):
baseError := errors.Cause(err)
return http.StatusBadRequest, ScriptErrorInsufficientFund, EncodeLink(baseError.Error())
case ledger.IsCompilationFailedError(err):
baseError := errors.Cause(err)
return http.StatusBadRequest, ScriptErrorCompilationFailed, EncodeLink(baseError.Error())
case ledger.IsScriptMetadataOverrideError(err):
baseError := errors.Cause(err)
return http.StatusBadRequest, ScriptErrorMetadataOverride, EncodeLink(baseError.Error())
case ledger.IsInvalidResourceResolutionError(err):
baseError := errors.Cause(err)
return http.StatusBadRequest, ResourceResolutionError, EncodeLink(baseError.Error())
case errors.Is(err, context.Canceled):
return http.StatusInternalServerError, ErrContextCancelled, ""
case storage.IsError(err):
case ledger.IsStorageError(err):
return http.StatusServiceUnavailable, ErrStore, ""
default:
return http.StatusInternalServerError, ErrInternal, ""
Expand Down
27 changes: 16 additions & 11 deletions pkg/api/controllers/account_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import (
"github.com/formancehq/ledger/pkg/storage"
ledgerstore "github.com/formancehq/ledger/pkg/storage/sqlstorage/ledger"
sharedapi "github.com/formancehq/stack/libs/go-libs/api"
"github.com/formancehq/stack/libs/go-libs/errorsutil"
"github.com/go-chi/chi/v5"
"github.com/pkg/errors"
)

func CountAccounts(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -44,22 +46,22 @@ func GetAccounts(w http.ResponseWriter, r *http.Request) {
r.URL.Query().Get("balance") != "" ||
r.URL.Query().Get(QueryKeyBalanceOperator) != "" ||
r.URL.Query().Get(QueryKeyPageSize) != "" {
apierrors.ResponseError(w, r, ledger.NewValidationError(
fmt.Sprintf("no other query params can be set with '%s'", QueryKeyCursor)))
apierrors.ResponseError(w, r, errorsutil.NewError(ledger.ErrValidation,
errors.Errorf("no other query params can be set with '%s'", QueryKeyCursor)))
return
}

res, err := base64.RawURLEncoding.DecodeString(r.URL.Query().Get(QueryKeyCursor))
if err != nil {
apierrors.ResponseError(w, r, ledger.NewValidationError(
fmt.Sprintf("invalid '%s' query param", QueryKeyCursor)))
apierrors.ResponseError(w, r, errorsutil.NewError(ledger.ErrValidation,
errors.Errorf("invalid '%s' query param", QueryKeyCursor)))
return
}

token := ledgerstore.AccountsPaginationToken{}
if err := json.Unmarshal(res, &token); err != nil {
apierrors.ResponseError(w, r, ledger.NewValidationError(
fmt.Sprintf("invalid '%s' query param", QueryKeyCursor)))
apierrors.ResponseError(w, r, errorsutil.NewError(ledger.ErrValidation,
errors.Errorf("invalid '%s' query param", QueryKeyCursor)))
return
}

Expand All @@ -76,8 +78,8 @@ func GetAccounts(w http.ResponseWriter, r *http.Request) {
balance := r.URL.Query().Get("balance")
if balance != "" {
if _, err := strconv.ParseInt(balance, 10, 64); err != nil {
apierrors.ResponseError(w, r, ledger.NewValidationError(
"invalid parameter 'balance', should be a number"))
apierrors.ResponseError(w, r, errorsutil.NewError(ledger.ErrValidation,
errors.New("invalid parameter 'balance', should be a number")))
return
}
}
Expand Down Expand Up @@ -116,7 +118,8 @@ func GetAccount(w http.ResponseWriter, r *http.Request) {
l := LedgerFromContext(r.Context())

if !core.ValidateAddress(chi.URLParam(r, "address")) {
apierrors.ResponseError(w, r, ledger.NewValidationError("invalid account address format"))
apierrors.ResponseError(w, r, errorsutil.NewError(ledger.ErrValidation,
errors.New("invalid account address format")))
return
}

Expand All @@ -135,13 +138,15 @@ func PostAccountMetadata(w http.ResponseWriter, r *http.Request) {
l := LedgerFromContext(r.Context())

if !core.ValidateAddress(chi.URLParam(r, "address")) {
apierrors.ResponseError(w, r, ledger.NewValidationError("invalid account address format"))
apierrors.ResponseError(w, r, errorsutil.NewError(ledger.ErrValidation,
errors.New("invalid account address format")))
return
}

var m core.Metadata
if err := json.NewDecoder(r.Body).Decode(&m); err != nil {
apierrors.ResponseError(w, r, ledger.NewValidationError("invalid metadata format"))
apierrors.ResponseError(w, r, errorsutil.NewError(ledger.ErrValidation,
errors.New("invalid metadata format")))
return
}

Expand Down
15 changes: 8 additions & 7 deletions pkg/api/controllers/balance_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ package controllers
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"

"github.com/formancehq/ledger/pkg/api/apierrors"
"github.com/formancehq/ledger/pkg/ledger"
"github.com/formancehq/ledger/pkg/storage"
ledgerstore "github.com/formancehq/ledger/pkg/storage/sqlstorage/ledger"
sharedapi "github.com/formancehq/stack/libs/go-libs/api"
"github.com/formancehq/stack/libs/go-libs/errorsutil"
"github.com/pkg/errors"
)

func GetBalancesAggregated(w http.ResponseWriter, r *http.Request) {
Expand All @@ -37,22 +38,22 @@ func GetBalances(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("after") != "" ||
r.URL.Query().Get("address") != "" ||
r.URL.Query().Get(QueryKeyPageSize) != "" {
apierrors.ResponseError(w, r, ledger.NewValidationError(
fmt.Sprintf("no other query params can be set with '%s'", QueryKeyCursor)))
apierrors.ResponseError(w, r, errorsutil.NewError(ledger.ErrValidation,
errors.Errorf("no other query params can be set with '%s'", QueryKeyCursor)))
return
}

res, err := base64.RawURLEncoding.DecodeString(r.URL.Query().Get(QueryKeyCursor))
if err != nil {
apierrors.ResponseError(w, r, ledger.NewValidationError(
fmt.Sprintf("invalid '%s' query param", QueryKeyCursor)))
apierrors.ResponseError(w, r, errorsutil.NewError(ledger.ErrValidation,
errors.Errorf("invalid '%s' query param", QueryKeyCursor)))
return
}

token := ledgerstore.BalancesPaginationToken{}
if err := json.Unmarshal(res, &token); err != nil {
apierrors.ResponseError(w, r, ledger.NewValidationError(
fmt.Sprintf("invalid '%s' query param", QueryKeyCursor)))
apierrors.ResponseError(w, r, errorsutil.NewError(ledger.ErrValidation,
errors.Errorf("invalid '%s' query param", QueryKeyCursor)))
return
}

Expand Down
23 changes: 12 additions & 11 deletions pkg/api/controllers/ledger_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package controllers
import (
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"strconv"

Expand All @@ -13,7 +12,9 @@ import (
"github.com/formancehq/ledger/pkg/storage"
ledgerstore "github.com/formancehq/ledger/pkg/storage/sqlstorage/ledger"
sharedapi "github.com/formancehq/stack/libs/go-libs/api"
"github.com/formancehq/stack/libs/go-libs/errorsutil"
"github.com/go-chi/chi/v5"
"github.com/pkg/errors"
)

type Info struct {
Expand Down Expand Up @@ -64,22 +65,22 @@ func GetLogs(w http.ResponseWriter, r *http.Request) {
r.URL.Query().Get(QueryKeyStartTime) != "" ||
r.URL.Query().Get(QueryKeyEndTime) != "" ||
r.URL.Query().Get(QueryKeyPageSize) != "" {
apierrors.ResponseError(w, r, ledger.NewValidationError(
fmt.Sprintf("no other query params can be set with '%s'", QueryKeyCursor)))
apierrors.ResponseError(w, r, errorsutil.NewError(ledger.ErrValidation,
errors.Errorf("no other query params can be set with '%s'", QueryKeyCursor)))
return
}

res, err := base64.RawURLEncoding.DecodeString(r.URL.Query().Get(QueryKeyCursor))
if err != nil {
apierrors.ResponseError(w, r, ledger.NewValidationError(
fmt.Sprintf("invalid '%s' query param", QueryKeyCursor)))
apierrors.ResponseError(w, r, errorsutil.NewError(ledger.ErrValidation,
errors.Errorf("invalid '%s' query param", QueryKeyCursor)))
return
}

token := ledgerstore.LogsPaginationToken{}
if err := json.Unmarshal(res, &token); err != nil {
apierrors.ResponseError(w, r, ledger.NewValidationError(
fmt.Sprintf("invalid '%s' query param", QueryKeyCursor)))
apierrors.ResponseError(w, r, errorsutil.NewError(ledger.ErrValidation,
errors.Errorf("invalid '%s' query param", QueryKeyCursor)))
return
}

Expand All @@ -95,8 +96,8 @@ func GetLogs(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("after") != "" {
afterIDParsed, err = strconv.ParseUint(r.URL.Query().Get("after"), 10, 64)
if err != nil {
apierrors.ResponseError(w, r, ledger.NewValidationError(
"invalid 'after' query param"))
apierrors.ResponseError(w, r, errorsutil.NewError(ledger.ErrValidation,
errors.New("invalid 'after' query param")))
return
}
}
Expand All @@ -105,15 +106,15 @@ func GetLogs(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get(QueryKeyStartTime) != "" {
startTimeParsed, err = core.ParseTime(r.URL.Query().Get(QueryKeyStartTime))
if err != nil {
apierrors.ResponseError(w, r, ErrInvalidStartTime)
apierrors.ResponseError(w, r, errorsutil.NewError(ledger.ErrValidation, ErrInvalidStartTime))
return
}
}

if r.URL.Query().Get(QueryKeyEndTime) != "" {
endTimeParsed, err = core.ParseTime(r.URL.Query().Get(QueryKeyEndTime))
if err != nil {
apierrors.ResponseError(w, r, ErrInvalidEndTime)
apierrors.ResponseError(w, r, errorsutil.NewError(ledger.ErrValidation, ErrInvalidEndTime))
return
}
}
Expand Down
14 changes: 8 additions & 6 deletions pkg/api/controllers/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (

"github.com/formancehq/ledger/pkg/ledger"
"github.com/formancehq/ledger/pkg/storage"
"github.com/formancehq/stack/libs/go-libs/errorsutil"
"github.com/pkg/errors"
)

const (
Expand All @@ -20,11 +22,11 @@ const (
)

var (
ErrInvalidPageSize = ledger.NewValidationError("invalid 'pageSize' query param")
ErrInvalidBalanceOperator = ledger.NewValidationError(
ErrInvalidPageSize = errors.New("invalid 'pageSize' query param")
ErrInvalidBalanceOperator = errors.New(
"invalid parameter 'balanceOperator', should be one of 'e, ne, gt, gte, lt, lte'")
ErrInvalidStartTime = ledger.NewValidationError("invalid 'startTime' query param")
ErrInvalidEndTime = ledger.NewValidationError("invalid 'endTime' query param")
ErrInvalidStartTime = errors.New("invalid 'startTime' query param")
ErrInvalidEndTime = errors.New("invalid 'endTime' query param")
)

func getPageSize(w http.ResponseWriter, r *http.Request) (uint, error) {
Expand All @@ -38,7 +40,7 @@ func getPageSize(w http.ResponseWriter, r *http.Request) (uint, error) {
if pageSizeParam != "" {
pageSize, err = strconv.ParseUint(pageSizeParam, 10, 32)
if err != nil {
return 0, ErrInvalidPageSize
return 0, errorsutil.NewError(ledger.ErrValidation, ErrInvalidPageSize)
}
}

Expand All @@ -55,7 +57,7 @@ func getBalanceOperator(w http.ResponseWriter, r *http.Request) (storage.Balance
if balanceOperatorStr != "" {
var ok bool
if balanceOperator, ok = storage.NewBalanceOperator(balanceOperatorStr); !ok {
return "", ErrInvalidBalanceOperator
return "", errorsutil.NewError(ledger.ErrValidation, ErrInvalidBalanceOperator)
}
}

Expand Down
Loading

0 comments on commit e821b75

Please sign in to comment.