mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 13:52:51 +00:00
# Which Problems Are Solved As part of https://github.com/zitadel/zitadel/issues/11917 we want to introduce error slugs so (API) clients can rely on stable, machine-readable errors and act accordingly. # How the Problems Are Solved - Added a `NewSlug` helper function in the domain package. - Added `ErrorDetails` to the `ZitadelError` - Added an `zitadel.error.v2.ErrorDetail` proto message - Updated the connectRPC error interceptor to map new slug based errors to the new `ErrorDetail` - Defined some common slugs and error functions like internal errors - Defined (session) specific slugs used in the `DeleteSession` and `CheckUser` functions and replaced old implementations - Updated integration tests to check specific errors if the relation database feature is enabled - Updated doc and guideline to reflect the latest changes and decisions - Updated DeleteSession endpoint API to list possible slugs # Additional Changes None # Additional Context - closes #11957 --------- Co-authored-by: Wim Van Laer <[email protected]>
295 lines
8.0 KiB
Go
295 lines
8.0 KiB
Go
package zerrors
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"runtime/debug"
|
|
"sync/atomic"
|
|
|
|
"github.com/zitadel/sloggcp"
|
|
)
|
|
|
|
//go:generate enumer -type=Kind -trimprefix=Kind -json
|
|
type Kind int
|
|
|
|
const (
|
|
// KindCanceled indicates that the operation was canceled, typically by the
|
|
// caller.
|
|
KindCanceled Kind = 1
|
|
|
|
// KindUnknown indicates that the operation failed for an unknown reason.
|
|
KindUnknown Kind = 2
|
|
|
|
// KindInvalidArgument indicates that client supplied an invalid argument.
|
|
KindInvalidArgument Kind = 3
|
|
|
|
// KindDeadlineExceeded indicates that deadline expired before the operation
|
|
// could complete.
|
|
KindDeadlineExceeded Kind = 4
|
|
|
|
// KindNotFound indicates that some requested entity (for example, a file or
|
|
// directory) was not found.
|
|
KindNotFound Kind = 5
|
|
|
|
// KindAlreadyExists indicates that client attempted to create an entity (for
|
|
// example, a file or directory) that already exists.
|
|
KindAlreadyExists Kind = 6
|
|
|
|
// KindPermissionDenied indicates that the caller doesn't have permission to
|
|
// execute the specified operation.
|
|
KindPermissionDenied Kind = 7
|
|
|
|
// KindResourceExhausted indicates that some resource has been exhausted. For
|
|
// example, a per-user quota may be exhausted or the entire file system may
|
|
// be full.
|
|
KindResourceExhausted Kind = 8
|
|
|
|
// KindPreconditionFailed indicates that the system is not in a state
|
|
// required for the operation's execution.
|
|
KindPreconditionFailed Kind = 9
|
|
|
|
// KindAborted indicates that operation was aborted by the system, usually
|
|
// because of a concurrency issue such as a sequencer check failure or
|
|
// transaction abort.
|
|
KindAborted Kind = 10
|
|
|
|
// KindOutOfRange indicates that the operation was attempted past the valid
|
|
// range (for example, seeking past end-of-file).
|
|
KindOutOfRange Kind = 11
|
|
|
|
// KindUnimplemented indicates that the operation isn't implemented,
|
|
// supported, or enabled in this service.
|
|
KindUnimplemented Kind = 12
|
|
|
|
// KindInternal indicates that some invariants expected by the underlying
|
|
// system have been broken. This Kind is reserved for serious errors.
|
|
KindInternal Kind = 13
|
|
|
|
// KindUnavailable indicates that the service is currently unavailable. This
|
|
// is usually temporary, so clients can back off and retry idempotent
|
|
// operations.
|
|
KindUnavailable Kind = 14
|
|
|
|
// KindDataLoss indicates that the operation has resulted in unrecoverable
|
|
// data loss or corruption.
|
|
KindDataLoss Kind = 15
|
|
|
|
// KindUnauthenticated indicates that the request does not have valid
|
|
// authentication credentials for the operation.
|
|
KindUnauthenticated Kind = 16
|
|
)
|
|
|
|
// IDRecover is the error ID used for errors created after recovery from panics.
|
|
const IDRecover = "RECOVER"
|
|
|
|
// Because errors are created through singletons, config is global.
|
|
var (
|
|
enableReportLocation atomic.Bool
|
|
enableStackTrace atomic.Bool
|
|
gcpErrorReporting atomic.Bool
|
|
)
|
|
|
|
// EnableReportLocation enables or disables report locations for created errors.
|
|
func EnableReportLocation(enable bool) {
|
|
enableReportLocation.Store(enable)
|
|
}
|
|
|
|
// EnableStackTrace enables or disables stack traces for created errors.
|
|
func EnableStackTrace(enable bool) {
|
|
enableStackTrace.Store(enable)
|
|
}
|
|
|
|
// GCPErrorReportingEnabled enables or disables special handling for GCP Error Reporting.
|
|
// It must be enabled when using a sloggcp handler to avoid duplicate report locations and stack traces.
|
|
func GCPErrorReportingEnabled(enable bool) {
|
|
gcpErrorReporting.Store(enable)
|
|
}
|
|
|
|
type ZitadelError struct {
|
|
Kind Kind
|
|
Parent error
|
|
Message string
|
|
ID string
|
|
Details ErrorDetails
|
|
|
|
// location where the error was created
|
|
reportLocation *sloggcp.ReportLocation
|
|
// stack trace at the point the error was created
|
|
stackTrace []byte
|
|
hasStackTrace bool
|
|
}
|
|
|
|
type Slug string
|
|
|
|
type ErrorDetails json.Marshaler
|
|
|
|
type ErrorDetailsMap map[string]any
|
|
|
|
func (m ErrorDetailsMap) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(map[string]any(m))
|
|
}
|
|
|
|
func ThrowError(parent error, id, message string) error {
|
|
return CreateZitadelError(KindUnknown, parent, id, message, 1)
|
|
}
|
|
|
|
// CreateZitadelError creates a ZitadelError of the given kind.
|
|
// If ReportLocation or StackTrace are enabled, they are added to the error.
|
|
// The skip parameter defines how many stack frames to skip when determining
|
|
// the report location.
|
|
// A skip value of 0 means the location where CreateZitadelError is called.
|
|
// When the parent error is also a ZitadelError, the report location and stack trace
|
|
// are inherited from the parent error.
|
|
func CreateZitadelError(kind Kind, parent error, id, message string, skip int) *ZitadelError {
|
|
err := &ZitadelError{
|
|
Kind: kind,
|
|
Parent: parent,
|
|
ID: id,
|
|
Message: message,
|
|
}
|
|
var target *ZitadelError
|
|
if errors.As(parent, &target) {
|
|
// inherit stack trace and report location from parent error
|
|
err.reportLocation = target.reportLocation
|
|
err.stackTrace = target.stackTrace
|
|
err.hasStackTrace = target.hasStackTrace
|
|
return err
|
|
}
|
|
|
|
if enableReportLocation.Load() {
|
|
err.reportLocation = sloggcp.NewReportLocation(skip + 1)
|
|
}
|
|
if enableStackTrace.Load() {
|
|
err.stackTrace = debug.Stack()
|
|
err.hasStackTrace = true
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (err *ZitadelError) Error() string {
|
|
if err.Parent != nil {
|
|
return fmt.Sprintf("ID=%s Message=%s Parent=(%v)", err.ID, err.Message, err.Parent)
|
|
}
|
|
return fmt.Sprintf("ID=%s Message=%s", err.ID, err.Message)
|
|
}
|
|
|
|
func (err *ZitadelError) Unwrap() error {
|
|
return err.GetParent()
|
|
}
|
|
|
|
func (err *ZitadelError) GetParent() error {
|
|
return err.Parent
|
|
}
|
|
|
|
func (err *ZitadelError) GetMessage() string {
|
|
return err.Message
|
|
}
|
|
|
|
func (err *ZitadelError) SetMessage(msg string) {
|
|
err.Message = msg
|
|
}
|
|
|
|
func (err *ZitadelError) GetID() string {
|
|
return err.ID
|
|
}
|
|
|
|
func (err *ZitadelError) GetDetails() ErrorDetails {
|
|
return err.Details
|
|
}
|
|
|
|
func (err *ZitadelError) WithDetails(details ErrorDetails) *ZitadelError {
|
|
err.Details = details
|
|
return err
|
|
}
|
|
|
|
func (err *ZitadelError) Is(target error) bool {
|
|
t, ok := target.(*ZitadelError)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if t.Kind != err.Kind {
|
|
return false
|
|
}
|
|
if t.ID != "" && t.ID != err.ID {
|
|
return false
|
|
}
|
|
if t.Message != "" && t.Message != err.Message {
|
|
return false
|
|
}
|
|
if t.Parent != nil && !errors.Is(err.Parent, t.Parent) {
|
|
return false
|
|
}
|
|
if t.Details != nil {
|
|
if err.Details == nil {
|
|
return false
|
|
}
|
|
targetData, detailsErr := t.Details.MarshalJSON()
|
|
if detailsErr != nil {
|
|
return false
|
|
}
|
|
errData, detailsErr := err.Details.MarshalJSON()
|
|
if detailsErr != nil {
|
|
return false
|
|
}
|
|
return bytes.Equal(targetData, errData)
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func IsZitadelError(err error) bool {
|
|
zitadelErr := new(ZitadelError)
|
|
return errors.As(err, &zitadelErr)
|
|
}
|
|
|
|
func AsZitadelError(err error) (*ZitadelError, bool) {
|
|
zitadelErr := new(ZitadelError)
|
|
ok := errors.As(err, &zitadelErr)
|
|
return zitadelErr, ok
|
|
}
|
|
|
|
var _ slog.LogValuer = (*ZitadelError)(nil)
|
|
|
|
func (err *ZitadelError) LogValue() slog.Value {
|
|
attributes := make([]slog.Attr, 0, 6)
|
|
attributes = append(attributes,
|
|
slog.String("kind", err.Kind.String()),
|
|
slog.String("message", err.Message),
|
|
slog.String("id", err.ID),
|
|
)
|
|
if err.Parent != nil {
|
|
attributes = append(attributes, slog.Any("parent", err.Parent))
|
|
}
|
|
// if gcp error reporting is enabled, log the error as a group without
|
|
// report location and stack trace, as those are handled by the handler.
|
|
if gcpErrorReporting.Load() {
|
|
return slog.GroupValue(attributes...)
|
|
}
|
|
|
|
if err.reportLocation != nil {
|
|
attributes = append(attributes, slog.Any("reportLocation", err.reportLocation))
|
|
}
|
|
if err.hasStackTrace {
|
|
attributes = append(attributes, slog.String("stackTrace", string(err.stackTrace)))
|
|
}
|
|
return slog.GroupValue(attributes...)
|
|
}
|
|
|
|
// ReportLocation implements [sloggcp.ReportLocationError].
|
|
func (err *ZitadelError) ReportLocation() *sloggcp.ReportLocation {
|
|
return err.reportLocation
|
|
}
|
|
|
|
// StackTrace implements [sloggcp.StackTraceError].
|
|
func (err *ZitadelError) StackTrace() (trace []byte, ok bool) {
|
|
return err.stackTrace, err.hasStackTrace
|
|
}
|
|
|
|
var (
|
|
_ sloggcp.ReportLocationError = (*ZitadelError)(nil)
|
|
_ sloggcp.StackTraceError = (*ZitadelError)(nil)
|
|
)
|