feat(domain): introduce error slugs (#12030)

# 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]>
This commit is contained in:
Livio Spring
2026-04-13 09:47:04 +02:00
committed by GitHub
co-authored by Wim Van Laer
parent 5c9361f6c7
commit a2bf528ca0
18 changed files with 524 additions and 97 deletions
@@ -10,10 +10,13 @@ import (
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/protoadapt"
"google.golang.org/protobuf/types/known/structpb"
commandErrors "github.com/zitadel/zitadel/internal/command/errors"
"github.com/zitadel/zitadel/internal/zerrors"
errorpb "github.com/zitadel/zitadel/pkg/grpc/error/v2"
"github.com/zitadel/zitadel/pkg/grpc/message"
)
@@ -98,11 +101,35 @@ func Test_getErrorInfo(t *testing.T) {
}(),
result: &message.CredentialsCheckError{Id: "id", Message: "key", FailedAttempts: 26},
},
{
name: "parent error not nil wrapped zerrors.ZitadelError{} with id, message errorpb.ErrorDetail{}",
id: "id",
key: "key",
err: func() error {
err := fmt.Errorf("normal error")
zerr := &zerrors.ZitadelError{ID: "id", Message: "key"}
return fmt.Errorf("%w: %w", err, zerr)
}(),
result: &message.ErrorDetail{Id: "id", Message: "key"},
},
{
name: "parent error not nil wrapped zerrors.ZitadelError{} with slug, return errorpb.ErrorDetail{}",
id: "slug.reason",
key: "message",
err: func() error {
err := fmt.Errorf("normal error")
zerr := &zerrors.ZitadelError{ID: "slug.reason", Message: "message", Details: zerrors.ErrorDetailsMap{"some": "details"}}
return fmt.Errorf("%w: %w", err, zerr)
}(),
result: &errorpb.ErrorDetail{Slug: "slug.reason", Message: "message", Details: &structpb.Struct{Fields: map[string]*structpb.Value{"some": {Kind: &structpb.Value_StringValue{StringValue: "details"}}}}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
errorInfo := getErrorInfo(tt.id, tt.key, tt.err)
assert.Equal(t, tt.result, errorInfo)
errorInfo := getErrorInfo(t.Context(), tt.id, tt.key, tt.err)
if !proto.Equal(tt.result.(proto.Message), errorInfo.(proto.Message)) {
t.Errorf("getErrorInfo() = %v, want %v", errorInfo, tt.result)
}
})
}
}