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"})";
}