using Grpc.Core; namespace MFiles.Grpc.Samples; /// /// A failed M-Files gRPC call, with the M-Files error decoded from the response trailers. /// public sealed class MFilesException : Exception { private MFilesException( RpcException inner, int? errorCode, string? details, IReadOnlyList errors) : base(inner.Status.Detail is { Length: > 0 } detail ? detail : inner.Message, inner) { StatusCode = inner.StatusCode; ErrorCode = errorCode; Details = details; Errors = errors; if (errorCode is int code) HResult = code; } /// The gRPC status code: UNKNOWN for M-Files errors, UNAUTHENTICATED for a bad session. public StatusCode StatusCode { get; } /// /// The M-Files error code the call failed with (an HRESULT, signed): the code of the /// top-level error, the last entry of the error stack, or of the last entry that is not the /// generic 0x80040001. Null if the server sent no error stack, e.g. for UNAUTHENTICATED. /// public int? ErrorCode { get; } /// The M-Files error code as unsigned hex, e.g. "0x8004000B", or null. public string? ErrorCodeHex => ErrorCode is int code ? FormatErrorCode(code) : null; /// The long error message (mfiles-error-text-bin), one line per stack entry, or null. public string? Details { get; } /// /// The error stack (mfiles-error-serialized-bin): the original error first, the error the /// call failed with last. Empty when the server sent none. /// public IReadOnlyList Errors { get; } /// The error codes of the stack as unsigned numbers, the original error first. public IReadOnlyList ErrorCodes => Errors.Select(e => unchecked((uint)e.ErrorCode)).ToList(); /// True if any error in the stack has the code , e.g. 0x8004000B. public bool Has(uint errorCode) => ErrorCodes.Contains(errorCode); /// Formats an M-Files error code as unsigned hex, e.g. "0x800407E4". public static string FormatErrorCode(int errorCode) => $"0x{unchecked((uint)errorCode):X8}"; // [START decode-error] /// The generic code of an error that is only passed up the stack. private const int PassThrough = unchecked((int)0x80040001); /// /// Decodes the M-Files error from the trailers of a failed call. /// public static MFilesException FromRpcException(RpcException e) { // The error stack: a serialized MFiles.RPCErrorInfoArray, the original error first. var errors = e.Trailers.GetValueBytes("mfiles-error-serialized-bin") is { } stack ? RPCErrorInfoArray.Parser.ParseFrom(stack).Value.ToList() : []; // The long error message: UTF-8 text in a binary header. string? details = e.Trailers.GetValueBytes("mfiles-error-text-bin") is { } text ? System.Text.Encoding.UTF8.GetString(text) : null; // The call failed with the top-level error, the last entry; the status detail is its // message. Skip entries that only pass the error on. int? errorCode = errors.Count > 0 ? errors[^1].ErrorCode : null; if (errors.LastOrDefault(error => error.ErrorCode != PassThrough) is { } topLevel) errorCode = topLevel.ErrorCode; return new MFilesException(e, errorCode, details, errors); } // [END decode-error] /// public override string ToString() => $"{GetType().FullName}: {Message} (status {StatusCode}, error {ErrorCodeHex ?? "none"})"; }