Files
r3/handler/handler_error.go
2025-02-09 22:13:26 +01:00

240 lines
7.8 KiB
Go

package handler
import (
"encoding/json"
"errors"
"fmt"
"regexp"
"slices"
"github.com/gofrs/uuid"
"github.com/jackc/pgx/v5/pgconn"
)
type errExpected struct {
context string
matchRx *regexp.Regexp // regex that matches the expected error message
number int
}
const (
// legacy, to be replaced
ErrAuthFailed = "authentication failed"
ErrBruteforceBlock = "blocked assumed bruteforce attempt"
ErrGeneral = "general error"
ErrUnauthorized = "unauthorized"
ErrWsClientChanFull = "client channel is full, dropping response"
// error contexts
ErrContextApp = "APP"
ErrContextCsv = "CSV"
ErrContextDbs = "DBS"
ErrContextLic = "LIC"
ErrContextSec = "SEC"
// error codes
ErrCodeAppUnknown int = 1
ErrCodeAppContextExceeded int = 2
ErrCodeAppContextCanceled int = 3
ErrCodeAppPresetProtected int = 4
ErrCodeAppNameEmpty int = 5
ErrCodeAppNameInvalid int = 6
ErrCodeAppUnknownModule int = 7
ErrCodeAppUnknownRelation int = 8
ErrCodeAppUnknownAttribute int = 9
ErrCodeCsvParseInt int = 1
ErrCodeCsvParseFloat int = 2
ErrCodeCsvParseDateTime int = 3
ErrCodeCsvBadAttributeType int = 4
ErrCodeCsvWrongFieldNumber int = 5
ErrCodeCsvEncryptedAttribute int = 6
ErrCodeDbsFunctionMessage int = 1
ErrCodeDbsConstraintUnique int = 2
ErrCodeDbsConstraintUniqueLogin int = 3
ErrCodeDbsConstraintFk int = 4
ErrCodeDbsConstraintNotNull int = 5
ErrCodeDbsIndexFailUnique int = 6 // special: is applied on frontend only, if ErrCodeDbsConstraintUnique is used but ID is unknown
ErrCodeDbsInvalidTypeSyntax int = 7
ErrCodeDbsChangedCachePlan int = 8
ErrCodeLicValidityExpired int = 1
ErrCodeLicLoginsReached int = 2
ErrCodeSecUnauthorized int = 1
ErrCodeSecDataKeysNotAvailable int = 5
ErrCodeSecNoPublicKeys int = 6
)
var (
// errors
errContexts = []string{ErrContextApp, ErrContextCsv, ErrContextDbs, ErrContextLic, ErrContextSec}
errCodeDbsCache = regexp.MustCompile(fmt.Sprintf("^{ERR_DBS_%03d}", ErrCodeDbsChangedCachePlan))
errCodeLicRx = regexp.MustCompile(`^{ERR_LIC_(\d{3})}`)
errCodeRx = regexp.MustCompile(`^{ERR_([A-Z]{3})_(\d{3})}`)
errExpectedList = []errExpected{
// security/access
{ // unauthorized
context: ErrContextSec,
matchRx: regexp.MustCompile(fmt.Sprintf(`^%s$`, ErrUnauthorized)),
number: ErrCodeSecUnauthorized,
},
// application
{ // context deadline reached
context: ErrContextApp,
matchRx: regexp.MustCompile(`^timeout\: context deadline exceeded$`),
number: ErrCodeAppContextExceeded,
},
{ // context canceled
context: ErrContextApp,
matchRx: regexp.MustCompile(`^timeout\: context canceled$`),
number: ErrCodeAppContextCanceled,
},
// CSV handling
{ // wrong number of fields (error originates from encoding/csv package)
context: ErrContextCsv,
matchRx: regexp.MustCompile(`^record on line \d+\: wrong number of fields`),
number: ErrCodeCsvWrongFieldNumber,
},
}
)
// creates standardized error code, to be interpreted and translated on the frontend
// context is the general error context: APP (application), DBS (database system), SEC (security/access), ...
// number is the unique error code, used to convert to a translated error message
// example error code: {ERR_DBS_069}
func CreateErrCode(context string, number int) error {
if !slices.Contains(errContexts, context) {
return errors.New("{INVALID_ERROR_CONTEXT}")
}
return fmt.Errorf("{ERR_%s_%03d}", context, number)
}
// as CreateErrCode, but appends JSON encoded data to the string
func CreateErrCodeWithData(context string, number int, data interface{}) error {
code := CreateErrCode(context, number)
j, err := json.Marshal(data)
if err != nil {
return code
}
return fmt.Errorf("%s%s", code, j)
}
// converts expected errors to error codes to be parsed/translated by requestor
// can optionally convert to generic 'unknown error' if error cannot be identified
// returns whether the error was identified
func ConvertToErrCode(err error, anonymizeIfUnexpected bool) (error, bool) {
var processUnexpectedErr = func(err error) error {
if anonymizeIfUnexpected {
return CreateErrCode(ErrContextApp, ErrCodeAppUnknown)
}
return err
}
// already an error code, return as is
if errCodeRx.MatchString(err.Error()) {
return err, true
}
// check for PGX error
var pgxErr *pgconn.PgError
if errors.As(err, &pgxErr) {
switch pgxErr.Code {
case "0A000": // error in prepared statement cache due to changed schema
return CreateErrCode(ErrContextDbs, ErrCodeDbsChangedCachePlan), true
case "23502": // NOT NULL constraint failure
return CreateErrCodeWithData(ErrContextDbs, ErrCodeDbsConstraintNotNull, struct {
ModuleName string `json:"moduleName"`
RelationName string `json:"relationName"`
AttributeName string `json:"attributeName"`
}{
pgxErr.SchemaName,
pgxErr.TableName,
pgxErr.ColumnName,
}), true
case "23503": // foreign key constraint failure
// foreign key constraint names have this format: "fk_[UUID]"
if pgxErr.ConstraintName == "" || pgxErr.ConstraintName[0:3] != "fk_" {
return processUnexpectedErr(err), false
}
return CreateErrCodeWithData(ErrContextDbs, ErrCodeDbsConstraintFk, struct {
AttributeId string `json:"attributeId"`
}{pgxErr.ConstraintName[3:]}), true
case "23505": // unique index constraint failure
// special case: login name index
if pgxErr.ConstraintName == "login_name_key" {
return CreateErrCode(ErrContextDbs, ErrCodeDbsConstraintUniqueLogin), true
}
// unique index constraint names have this format: "ind_[UUID]"
if pgxErr.ConstraintName == "" || pgxErr.ConstraintName[0:4] != "ind_" {
return processUnexpectedErr(err), false
}
return CreateErrCodeWithData(ErrContextDbs, ErrCodeDbsConstraintUnique, struct {
PgIndexId string `json:"pgIndexId"`
}{pgxErr.ConstraintName[4:]}), true
case "22P02": // invalid type syntax
return CreateErrCode(ErrContextDbs, ErrCodeDbsInvalidTypeSyntax), true
case "P0001": // exception raised
if pgxErr.Message == "" || pgxErr.Message[0:6] != "R3_MSG" {
return processUnexpectedErr(err), false
}
return CreateErrCodeWithData(ErrContextDbs, ErrCodeDbsFunctionMessage, struct {
Message string `json:"message"`
}{pgxErr.Message[8:]}), true
}
}
// check for match against expected errors
for _, expErr := range errExpectedList {
if expErr.matchRx.MatchString(err.Error()) {
return CreateErrCode(expErr.context, expErr.number), true
}
}
return processUnexpectedErr(err), false
}
// error code checker
func CheckForLicenseErrCode(err error) bool {
return errCodeLicRx.MatchString(err.Error())
}
func CheckForDbsCacheErrCode(err error) bool {
return errCodeDbsCache.MatchString(err.Error())
}
// default schema errors
func ErrSchemaUnknownModule(id uuid.UUID) error {
return fmt.Errorf("unknown module '%s'", id)
}
func ErrSchemaUnknownRelation(id uuid.UUID) error {
return fmt.Errorf("unknown relation '%s'", id)
}
func ErrSchemaUnknownAttribute(id uuid.UUID) error {
return fmt.Errorf("unknown attribute '%s'", id)
}
func ErrSchemaUnknownFunction(id uuid.UUID) error {
return fmt.Errorf("unknown function '%s'", id)
}
func ErrSchemaUnknownPolicyAction(name string) error {
return fmt.Errorf("unknown policy action '%s'", name)
}
func ErrSchemaUnknownClientEvent(id uuid.UUID) error {
return fmt.Errorf("unknown client event '%s'", id)
}
func ErrSchemaUnknownPgFunction(id uuid.UUID) error {
return fmt.Errorf("unknown backend function '%s'", id)
}
func ErrSchemaTriggerPgFunctionCall(id uuid.UUID) error {
return fmt.Errorf("backend function '%s' is a trigger function, it cannot be called directly", id)
}
func ErrSchemaBadFrontendExecPgFunctionCall(id uuid.UUID) error {
return fmt.Errorf("backend function '%s' may not be called from the frontend", id)
}