package mxgateway import ( "errors" "fmt" pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) // ErrEventBufferOverflow is the terminal error delivered on the compatibility // event channel returned by Session.Events / Session.EventsAfter when a slow // consumer lets the bounded result buffer fill. It signals that the stream was // cancelled and events were dropped, so a consumer can tell an overflow apart // from a normal end-of-stream. Use Session.SubscribeEvents to block instead of // dropping. var ErrEventBufferOverflow = errors.New("mxgateway: event buffer overflow; compatibility stream cancelled and events dropped") // GatewayError wraps transport-level gRPC failures. type GatewayError struct { // Op names the operation that failed (for example "dial" or "invoke"). Op string // Err is the underlying gRPC or transport error. Err error } // Error returns the formatted gateway error message. func (e *GatewayError) Error() string { if e == nil { return "" } if e.Op == "" { return fmt.Sprintf("mxgateway: %v", e.Err) } return fmt.Sprintf("mxgateway: %s failed: %v", e.Op, e.Err) } // Unwrap returns the wrapped transport error. func (e *GatewayError) Unwrap() error { if e == nil { return nil } return e.Err } // Code returns the gRPC status code of the wrapped transport error. It returns // codes.OK when the error is nil and codes.Unknown when the wrapped error does // not carry a gRPC status. Callers can use it to write retry, timeout, and // auth handling without manually unwrapping and re-parsing the error. func (e *GatewayError) Code() codes.Code { if e == nil || e.Err == nil { return codes.OK } return status.Code(e.Err) } // IsTransient reports whether err is a transport failure that may succeed on // retry — for example a gateway that is briefly Unavailable or a call that // hit a DeadlineExceeded. Permanent failures (Unauthenticated, PermissionDenied, // InvalidArgument, NotFound, and similar) return false. It unwraps through // *GatewayError and any other error chain carrying a gRPC status, so callers // do not need to call status.Code themselves. func IsTransient(err error) bool { if err == nil { return false } switch transientCode(err) { case codes.Unavailable, codes.DeadlineExceeded, codes.ResourceExhausted, codes.Aborted: return true default: return false } } // transientCode extracts a gRPC status code from err, preferring a wrapped // *GatewayError's Code and otherwise falling back to status.Code on the chain. func transientCode(err error) codes.Code { var gatewayErr *GatewayError if errors.As(err, &gatewayErr) { return gatewayErr.Code() } return status.Code(err) } // CommandError reports a non-OK gateway protocol status and keeps the raw // command reply when one exists. type CommandError struct { // Op names the gateway operation that produced the non-OK status. Op string // Status carries the gateway-reported protocol status. Status *ProtocolStatus // Reply is the raw command reply, when one was returned alongside the status. Reply *MxCommandReply } // Error returns the formatted command error message. func (e *CommandError) Error() string { if e == nil { return "" } status := e.Status if status == nil { return fmt.Sprintf("mxgateway: %s failed with missing protocol status", e.Op) } if status.GetMessage() == "" { return fmt.Sprintf("mxgateway: %s failed with protocol status %s", e.Op, status.GetCode()) } return fmt.Sprintf("mxgateway: %s failed with protocol status %s: %s", e.Op, status.GetCode(), status.GetMessage()) } // MxAccessError reports HRESULT or MXSTATUS_PROXY failures returned by MXAccess. type MxAccessError struct { // Command is the wrapped CommandError when the protocol status carried one. Command *CommandError // Reply is the raw MXAccess command reply that surfaced the failure. Reply *MxCommandReply } // Error returns the formatted MXAccess error message. func (e *MxAccessError) Error() string { if e == nil { return "" } if e.Command != nil && e.Command.Status != nil && e.Command.Status.GetMessage() != "" { return e.Command.Error() } if e.Reply != nil && e.Reply.GetDiagnosticMessage() != "" { return fmt.Sprintf("mxgateway: MXAccess command %s failed: %s", e.Reply.GetKind(), e.Reply.GetDiagnosticMessage()) } if e.Reply != nil && e.Reply.Hresult != nil { return fmt.Sprintf("mxgateway: MXAccess command %s failed with HRESULT 0x%08X", e.Reply.GetKind(), uint32(e.Reply.GetHresult())) } return "mxgateway: MXAccess command failed" } // Unwrap returns the wrapped CommandError, when one is present. // // When Command is nil (the HRESULT / MxStatusProxy path) it returns an // untyped nil rather than a typed-nil *CommandError, so errors.As does not // bind a nil pointer that a caller would then panic on. func (e *MxAccessError) Unwrap() error { if e == nil || e.Command == nil { return nil } return e.Command } // EnsureProtocolSuccess returns a typed CommandError when status is non-OK. func EnsureProtocolSuccess(op string, status *ProtocolStatus, reply *MxCommandReply) error { if status == nil || status.GetCode() == pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK { return nil } commandError := &CommandError{ Op: op, Status: status, Reply: reply, } if status.GetCode() == pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_MXACCESS_FAILURE { return &MxAccessError{ Command: commandError, Reply: reply, } } return commandError } // EnsureMxAccessSuccess returns a typed MxAccessError for failing HRESULTs or // MXSTATUS_PROXY entries. func EnsureMxAccessSuccess(op string, reply *MxCommandReply) error { if reply == nil { return nil } if reply.Hresult != nil && reply.GetHresult() != 0 { return &MxAccessError{Reply: reply} } for _, status := range reply.GetStatuses() { if !StatusSucceeded(status) { return &MxAccessError{Reply: reply} } } return nil }