Handle errors

View as Markdown

A failed call ends with a gRPC status. For M-Files errors the status is UNKNOWN, and the M-Files error code (an HRESULT) and the error stack travel in trailers. Decode them, act on the codes you handle, and report the rest.

How errors arrive

What Where
gRPC status UNKNOWN for M-Files errors; UNAUTHENTICATED for a missing or expired session (no trailers); UNAVAILABLE and others from the transport
Message The status message: the message of the top-level error
Error stack Trailer mfiles-error-serialized-bin: a serialized RPCErrorInfoArray, ordered from the original cause (first) to the top-level error (last)
Diagnostic text Trailer mfiles-error-text-bin: the stack as UTF-8 text, for logs

error_code is a signed 32-bit integer; mask it with 0xFFFFFFFF to compare it with codes written in hex. Details and the table of common codes: Errors.

What the helpers give you

The samples' helpers turn a failed call into an error object with the same parts in every language:

Part Python (MFilesError) C# (MFilesException) Go (*mfilesclient.Error)
Error code hresult, hresult_hex, hresult_signed ErrorCode, ErrorCodeHex HResult, SignedHResult()
All codes, cause first codes ErrorCodes Codes
Code anywhere in the stack has(code) Has(code) Has(code)
Message message Message Message
gRPC status status StatusCode Status

The error code is the code of the last entry of the stack, the top-level error that the status message describes. If that entry has the generic code 0x80040001, which only passes an error on, the code is that of the last entry that has another code. Without an error stack (for example UNAUTHENTICATED) there is no error code.

Test with "has". The code you handle can be anywhere in the stack, so test with has rather than comparing the error code. Decoding, in Python:

self.long_message: str = trailers.get(ERROR_TEXT_KEY, b"").decode("utf-8", "replace")
self.stack = list(structs_pb2.RPCErrorInfoArray.FromString(
    trailers.get(ERROR_STACK_KEY, b"")).value)
self.codes = [entry.error_code & 0xFFFFFFFF for entry in self.stack]  # cause first
# The top-level error, skipping entries that only pass the error on.
self.hresult: Optional[int] = next(
    (code for code in reversed(self.codes) if code != E_PASS_THROUGH),
    self.codes[-1] if self.codes else None)

Source: python/mfiles_client.py

Code

The recipe asks for an object that does not exist, then calls a method that needs vault administrator rights.

Python

# Not found: a document ID that does not exist.
objects = client.stub(IRPCObjectOperations_pb2_grpc.IRPCObjectOperationsStub)
try:
    objects.GetObjectInfoAndProperties(
        IRPCObjectOperations_pb2.GetObjectInfoAndPropertiesRequest(
            obj_id=obj_id(0, 999_999_999), latest_version=True))
    raise RuntimeError("Expected the call to fail.")
except MFilesError as error:
    if not error.has(E_NOT_FOUND):
        raise
    print_error("Not found", error)

# Access denied: an administrative call by a user who is not a vault administrator.
admin = client.stub(IRPCPropertyDefsAdmin_pb2_grpc.IRPCPropertyDefsAdminStub)
try:
    admin.GetPropertyDefsAdmin(IRPCPropertyDefsAdmin_pb2.GetPropertyDefsAdminRequest())
    print("Access denied: not provoked, the user is a vault administrator")
except MFilesError as error:
    if not error.has(E_ACCESS_DENIED):
        raise
    print_error("Access denied", error)

Source: python/recipes/errors.py

C#

// Not found: a document ID that does not exist.
var objects = new IRPCObjectOperations.IRPCObjectOperationsClient(connection.CallInvoker);
try
{
    await objects.GetObjectInfoAndPropertiesAsync(new GetObjectInfoAndPropertiesRequest
    {
        ObjId = ObjId(0, 999_999_999),
        LatestVersion = true,
    });
    throw new InvalidOperationException("Expected the call to fail.");
}
catch (MFilesException e) when (e.Has(NotFound))
{
    Print("Not found", e);
}

// Access denied: an administrative call by a user who is not a vault administrator.
var admin = new IRPCPropertyDefsAdmin.IRPCPropertyDefsAdminClient(connection.CallInvoker);
try
{
    await admin.GetPropertyDefsAdminAsync(new GetPropertyDefsAdminRequest());
    Console.WriteLine("Access denied: not provoked, the user is a vault administrator");
}
catch (MFilesException e) when (e.Has(AccessDenied))
{
    Print("Access denied", e);
}

Source: csharp/Recipes/Errors.cs

Go

// Not found: a document ID that does not exist.
objects := mfiles.NewIRPCObjectOperationsClient(client.Conn)
_, err = objects.GetObjectInfoAndProperties(ctx, &mfiles.GetObjectInfoAndPropertiesRequest{
	ObjId:         mfilesclient.ObjID(0, 999_999_999),
	LatestVersion: true,
})
if mfilesErr, ok := mfilesclient.AsError(err); ok && mfilesErr.Has(mfilesclient.CodeNotFound) {
	printError("Not found", mfilesErr)
} else {
	return fmt.Errorf("expected a not-found error, got %v", err)
}

// Access denied: an administrative call by a user who is not a vault administrator.
admin := mfiles.NewIRPCPropertyDefsAdminClient(client.Conn)
_, err = admin.GetPropertyDefsAdmin(ctx, &mfiles.GetPropertyDefsAdminRequest{})
switch mfilesErr, ok := mfilesclient.AsError(err); {
case err == nil:
	fmt.Println("Access denied: not provoked, the user is a vault administrator")
case ok && mfilesErr.Has(mfilesclient.CodeAccessDenied):
	printError("Access denied", mfilesErr)
default:
	return err
}

Source: go/recipes/errors/main.go

Output for a normal user (the stack lists each code once):

Not found: gRPC status UNKNOWN, M-Files error 0x8004000B (-2147221493)
  Message: Not found.
  Error stack, original error first: 0x8004000B
Access denied: gRPC status UNKNOWN, M-Files error 0x800407E4 (-2147219484)
  Message: Access denied. You do not have the access rights required by this operation.
  Error stack, original error first: 0x800400A9, 0x800407E4

The full access-denied stack is 0x800400A9 followed by six entries of 0x800407E4; the not-found stack is eight entries of 0x8004000B.

Pitfalls

See also