mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-20 06:24:25 +00:00
# Which Problems Are Solved Panics may cause a service discruption by unexpectedly closing an request's connection. Or in the case of gRPC completely killing the service. Allthough panics are still individual bugs that need to be solved, this PR makes sure a panic is gracefully handled and an understandable error is returned to the client. # How the Problems Are Solved - Recover in the middleware interceptors for the 3 API protocols (HTTP, gRPC, connect). - HTTP middleware uses formatted responses for: - OIDC errors (JSON formatted response) - UI (error page rendering) - SCIM - Upon recovery an alert level log is printed (ERROR+4 for stdlib log handlers) # Additional Context - internal observation --------- Co-authored-by: Livio Spring <[email protected]> Co-authored-by: Livio Spring <[email protected]>
141 lines
4.0 KiB
Go
141 lines
4.0 KiB
Go
package gerrors
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
|
|
"connectrpc.com/connect"
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
"github.com/zitadel/sloggcp"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/protoadapt"
|
|
|
|
"github.com/zitadel/zitadel/backend/v3/instrumentation/logging"
|
|
commandErrors "github.com/zitadel/zitadel/internal/command/errors"
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
"github.com/zitadel/zitadel/pkg/grpc/message"
|
|
)
|
|
|
|
func ZITADELToGRPCError(ctx context.Context, err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
code, key, id, lvl := extractError(err)
|
|
msg := key
|
|
msg += " (" + id + ")"
|
|
logging.Log(ctx, lvl, msg, "err", err, "code", code)
|
|
|
|
errorInfo := getErrorInfo(id, key, err)
|
|
|
|
s, err := status.New(code, msg).WithDetails(errorInfo)
|
|
if err != nil {
|
|
logging.WithError(ctx, err).Debug("unable to add error detail")
|
|
return status.New(code, msg).Err()
|
|
}
|
|
|
|
return s.Err()
|
|
}
|
|
|
|
func ZITADELToConnectError(ctx context.Context, err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
connectError := new(connect.Error)
|
|
if errors.As(err, &connectError) {
|
|
// Connect error may be returned by other middlewares,
|
|
// so we assume it's a client error and log as warning.
|
|
logging.Warn(ctx, connectError.Message(), "err", connectError.Unwrap(), "code", connectError.Code())
|
|
return err
|
|
}
|
|
code, key, id, lvl := extractError(err)
|
|
msg := key
|
|
msg += " (" + id + ")"
|
|
logging.Log(ctx, lvl, msg, "err", err)
|
|
|
|
errorInfo := getErrorInfo(id, key, err)
|
|
|
|
cErr := connect.NewError(connect.Code(code), errors.New(msg))
|
|
if detail, detailErr := connect.NewErrorDetail(errorInfo.(proto.Message)); detailErr == nil {
|
|
cErr.AddDetail(detail)
|
|
}
|
|
return cErr
|
|
}
|
|
|
|
func ExtractZITADELError(err error) (code codes.Code, msg, id string) {
|
|
if err == nil {
|
|
return codes.OK, "", ""
|
|
}
|
|
code, msg, id, _ = extractError(err)
|
|
return code, msg, id
|
|
}
|
|
|
|
func extractError(err error) (c codes.Code, msg, id string, lvl slog.Level) {
|
|
connErr := new(pgconn.ConnectError)
|
|
if ok := errors.As(err, &connErr); ok {
|
|
return codes.Internal, "db connection error", "", slog.LevelError
|
|
}
|
|
zitadelErr, ok := zerrors.AsZitadelError(err)
|
|
if !ok {
|
|
return codes.Unknown, err.Error(), "", slog.LevelError
|
|
}
|
|
msg, id = zitadelErr.GetMessage(), zitadelErr.GetID()
|
|
|
|
switch zitadelErr.Kind {
|
|
case zerrors.KindAlreadyExists:
|
|
c, lvl = codes.AlreadyExists, slog.LevelWarn
|
|
case zerrors.KindDeadlineExceeded:
|
|
c, lvl = codes.DeadlineExceeded, slog.LevelError
|
|
case zerrors.KindInternal:
|
|
c, lvl = codes.Internal, slog.LevelError
|
|
case zerrors.KindInvalidArgument:
|
|
c, lvl = codes.InvalidArgument, slog.LevelWarn
|
|
case zerrors.KindNotFound:
|
|
c, lvl = codes.NotFound, slog.LevelWarn
|
|
case zerrors.KindPermissionDenied:
|
|
c, lvl = codes.PermissionDenied, slog.LevelWarn
|
|
case zerrors.KindPreconditionFailed:
|
|
c, lvl = codes.FailedPrecondition, slog.LevelWarn
|
|
case zerrors.KindUnauthenticated:
|
|
c, lvl = codes.Unauthenticated, slog.LevelWarn
|
|
case zerrors.KindUnavailable:
|
|
c, lvl = codes.Unavailable, slog.LevelError
|
|
case zerrors.KindUnimplemented:
|
|
c, lvl = codes.Unimplemented, slog.LevelInfo
|
|
case zerrors.KindResourceExhausted:
|
|
c, lvl = codes.ResourceExhausted, slog.LevelError
|
|
case zerrors.KindDataLoss:
|
|
c, lvl = codes.DataLoss, slog.LevelError
|
|
case zerrors.KindCanceled:
|
|
c, lvl = codes.Canceled, slog.LevelWarn
|
|
case zerrors.KindAborted:
|
|
c, lvl = codes.Aborted, slog.LevelError
|
|
case zerrors.KindOutOfRange:
|
|
c, lvl = codes.OutOfRange, slog.LevelWarn
|
|
case zerrors.KindUnknown:
|
|
c, lvl = codes.Unknown, slog.LevelError
|
|
default:
|
|
c, lvl = codes.Unknown, slog.LevelError
|
|
}
|
|
if id == zerrors.IDRecover {
|
|
lvl = sloggcp.LevelAlert
|
|
}
|
|
return c, msg, id, lvl
|
|
}
|
|
|
|
func getErrorInfo(id, key string, err error) protoadapt.MessageV1 {
|
|
var errorInfo protoadapt.MessageV1
|
|
|
|
var wpe *commandErrors.WrongPasswordError
|
|
if err != nil && errors.As(err, &wpe) {
|
|
errorInfo = &message.CredentialsCheckError{Id: id, Message: key, FailedAttempts: wpe.FailedAttempts}
|
|
} else {
|
|
errorInfo = &message.ErrorDetail{Id: id, Message: key}
|
|
}
|
|
|
|
return errorInfo
|
|
}
|