# Go quickstart

Location: [gRPC API](https://developer.m-files.com/APIs/gRPC-API/index.md) > [Guides](https://developer.m-files.com/APIs/gRPC-API/index.md#guides)

HTML version: https://developer.m-files.com/APIs/gRPC-API/guides/quickstart-go/

In about 15 minutes you generate Go code from the M-Files protos, configure a connection and run
a program that logs in to a vault, lists its 10 most recently modified documents and logs out.
The program uses the official `google.golang.org/grpc` and `google.golang.org/protobuf` modules
and `mfilesclient`, a small connection helper from the samples that you can copy into your own
project.

## Prerequisites

* [Go](https://go.dev/dl/) 1.26 or later: `google.golang.org/grpc` v1.85.0 requires it.
* `protoc`, the protocol buffer compiler. Python's `grpcio-tools` package includes one
  (`pip install grpcio-tools`); the generate scripts use it when `protoc` is not on `PATH`.
* The Go plugins for `protoc`:

  ```shell
  go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.12
  go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.6.2
  ```

* A vault you can reach over gRPC, and its GUID with braces
  (`{7BBEA0BC-40D8-4BCB-A67D-8A3ACFBDB575}`). Don't know the GUID?
  [Find the vault by its name](https://developer.m-files.com/APIs/gRPC-API/guides/connect/index.md#find-the-vault-by-its-name) lists the vaults you can log
  in to, or ask your M-Files administrator.
  The endpoint:
  * **M-Files Cloud:** the vault's host name; the endpoint is `<vault host>:443` with TLS.
  * **On-premises:** the server's host name and gRPC port, `7766` by default; plain HTTP/2
    unless the administrator has enabled TLS.
* An M-Files user name and password, or a token from the vault's identity provider.

[Cloud and on-premises](https://developer.m-files.com/APIs/gRPC-API/runtime/deployments/index.md) and
[Connecting to the server](https://developer.m-files.com/APIs/gRPC-API/runtime/connecting/index.md) explain the endpoints in detail.

## Get the samples and the protos

Download the [gRPC API bundle](https://developer.m-files.com/APIs/gRPC-API/downloads/mfiles-grpc-api-bundle.zip) and unpack it. It contains:

* `proto/`: the API definition, one `.proto` file per service plus `mfiles/structs.proto`,
  `mfiles/enums.proto` and `mfiles/api/annotations.proto`, with the documentation as comments;
* `samples/go/`: the Go module this guide uses.

An M-Files Server installation also has the API definition, in
`<installation folder>\Common\Web\GRPC\proto`, as one combined legacy file without
documentation. It describes the same wire contract, but it does not produce the Go package path
these samples import. Prefer the bundle.

## Generate the code

```powershell
cd samples\go
.\generate.ps1          # PowerShell; in bash: ./generate.sh
```

The script runs `protoc` with `protoc-gen-go` and `protoc-gen-go-grpc` on every file of the
bundle and writes the code into `gen/`. It finds the bundle at `../../proto`; pass another folder
with `-ProtoDir` (or as the first argument of `generate.sh`), or set `MFILES_PROTO_DIR`.

The bundle's `go_package` option puts all services, messages and enums into one package,
`github.com/M-Files/mfiles-grpc-api/go/mfiles`. The script makes `gen/` a module of its own
with that path, and the samples' `go.mod` points to it:

```go
// The generated code: a module of its own in ./gen, written by generate.ps1 or generate.sh.
replace github.com/M-Files/mfiles-grpc-api => ./gen

```

*Source: [go/go.mod](https://developer.m-files.com/APIs/gRPC-API/downloads/samples/go/go.mod)*

Your code imports the package as `"github.com/M-Files/mfiles-grpc-api/go/mfiles"`. The
package is large: the first build takes a while, later builds come from the cache.

To start a project of your own, copy `gen/`, the `replace` directive and the `mfilesclient`
folder, and require `google.golang.org/grpc` and `google.golang.org/protobuf` in your `go.mod`.

## Configure

The samples read their settings from environment variables:

| Variable | Value |
|---|---|
| `MFILES_ADDRESS` | `host:port`, for example `contoso.cloudvault.m-files.com:443` or `mfserver:7766` |
| `MFILES_TLS` | `true` (default), or `false` for a server without TLS |
| `MFILES_CA_CERT` | Optional: a PEM file of root certificates to trust, for a server whose certificate comes from a private CA |
| `MFILES_VAULT` | The vault GUID, with braces |
| `MFILES_USERNAME`, `MFILES_PASSWORD` | An M-Files user account (M-Files authentication) |
| `MFILES_TOKEN`, `MFILES_AUTH_PLUGIN`, `MFILES_AUTH_SCOPE` | Instead of a password: a token from the vault's identity provider, the authentication plugin's name and its configuration scope |

M-Files Cloud (PowerShell):

```powershell
$env:MFILES_ADDRESS = 'contoso.cloudvault.m-files.com:443'
$env:MFILES_VAULT = '{7BBEA0BC-40D8-4BCB-A67D-8A3ACFBDB575}'
$env:MFILES_USERNAME = 'alice'
$env:MFILES_PASSWORD = Read-Host -Prompt 'Password'
```

On-premises without TLS (bash):

```shell
export MFILES_ADDRESS=mfserver:7766 MFILES_TLS=false
export MFILES_VAULT='{7BBEA0BC-40D8-4BCB-A67D-8A3ACFBDB575}'
export MFILES_USERNAME=alice
read -rs MFILES_PASSWORD && export MFILES_PASSWORD
```

Use `MFILES_TLS=false` only for a server that has no certificate, such as a lab installation.

**Single sign-on.** A vault that signs users in through M-Files Login Service, as is typical on
M-Files Cloud, refuses a password login with "Authentication failed.". Set `MFILES_TOKEN` to an
access token from the Login Service, and `MFILES_AUTH_PLUGIN` and `MFILES_AUTH_SCOPE` to the plugin
and scope that the vault returns.
[M-Files Login Service](https://developer.m-files.com/APIs/gRPC-API/runtime/authentication/index.md#m-files-login-service) shows how to get the
token; the Python sample `recipes/login_service.py` does it end to end.
Getting an SSO token is up to your application; see
[Authentication and sessions](https://developer.m-files.com/APIs/gRPC-API/runtime/authentication/index.md).

## Run

```shell
go run ./quickstart
```

The output lists the documents, newest first. In the M-Files Sample Vault it starts like this:

```text
Project Plan (template) (ID 445, version 1, modified 2012-06-07 12:01:57 UTC)
Project Tasks - CRM Development (ID 444, version 1, modified 2012-06-07 11:54:29 UTC)
Corporate Presentation (ID 443, version 1, modified 2012-06-07 11:40:14 UTC)
...
```

## The code

```go
// Connect and log in with the settings in the MFILES_* environment variables.
cfg, err := mfilesclient.ConfigFromEnv()
if err != nil {
	return err
}
cfg.ClientName = "M-Files gRPC quickstart"
client, err := mfilesclient.Connect(ctx, cfg)
if err != nil {
	return err
}
defer client.Close() // logs out and closes the channel

// Documents (object type 0) that are not deleted, sorted by "Last modified" (property 21),
// newest first. The server sorts before it applies the limit.
search := mfiles.NewIRPCSearchClient(client.Conn)
response, err := search.SearchObjects(ctx, &mfiles.SearchObjectsRequest{
	Conditions: mfilesclient.AllOf(mfilesclient.IsObjectType(0), mfilesclient.IsNotDeleted()),
	Sorting:    mfilesclient.SortBy(21, true),
	Limit:      10,
})
if err != nil {
	return err
}

for _, item := range response.GetResults() {
	object := item.GetObject()
	version := object.GetVersionInfo()
	fmt.Printf("%s (ID %d, version %d, modified %s UTC)\n",
		version.GetTitle(),
		object.GetObjectInfo().GetObjId().GetItemId().GetInternalId(),
		version.GetVersion().GetInternalVersion(),
		version.GetLastModifiedAtUtc().AsTime().Format(time.DateTime))
}
```

*Source: [go/quickstart/main.go](https://developer.m-files.com/APIs/gRPC-API/downloads/samples/go/quickstart/main.go)*

* `mfilesclient.Connect` creates the channel and logs in. `client.Close` logs out and closes the
  channel.
* Every service has a generated client, created on the channel:
  `mfiles.NewIRPCSearchClient(client.Conn)`.
* The search has one array of conditions; all conditions in an array must match. Without the
  "not deleted" condition, users who may see deleted objects get them too.
* `Limit: 10` with a sort order returns the first 10 of all matches in that order.
* A failed call returns an `*mfilesclient.Error` that names the M-Files error code.

`IsObjectType` and `IsNotDeleted` are small builders from the helper. They show the message
shapes of the two conditions:

```go

// StatusCondition compares a status value of the object, such as its object type or whether
// it is deleted.
func StatusCondition(status mfiles.StatusType, condition mfiles.ConditionType,
	value *mfiles.TypedValue) *mfiles.SearchCondition {
	return &mfiles.SearchCondition{
		Expression: &mfiles.Expression{
			Type: mfiles.ExpressionType_EXPRESSION_TYPE_STATUS_VALUE,
			Data: &mfiles.ExpressionUnion{Data: &mfiles.ExpressionUnion_StatusValue{
				StatusValue: &mfiles.ExpressionUnion_StatusValueData{Type: status},
			}},
		},
		Type:  condition,
		Value: value,
	}
}

// PropertyCondition compares a property value of the object, such as its class (property 100)
// or "Last modified" (property 21).
func PropertyCondition(propertyDef int32, condition mfiles.ConditionType,
	value *mfiles.TypedValue) *mfiles.SearchCondition {
	return &mfiles.SearchCondition{
		Expression: &mfiles.Expression{
			Type: mfiles.ExpressionType_EXPRESSION_TYPE_PROPERTY_VALUE,
			Data: &mfiles.ExpressionUnion{Data: &mfiles.ExpressionUnion_PropertyValue{
				PropertyValue: &mfiles.ExpressionUnion_PropertyValueData{PropertyDef: propertyDef},
			}},
		},
		Type:  condition,
		Value: value,
	}
}

// IsObjectType matches objects of one object type (0 = document). Value list -11 lists the
// object types.
func IsObjectType(objectType int32) *mfiles.SearchCondition {
	return StatusCondition(mfiles.StatusType_STATUS_TYPE_OBJECT_TYPE,
		mfiles.ConditionType_CONDITION_TYPE_EQUAL, Lookup(-11, objectType))
}

// IsNotDeleted matches objects that are not deleted.
func IsNotDeleted() *mfiles.SearchCondition {
	return StatusCondition(mfiles.StatusType_STATUS_TYPE_IS_DELETED,
		mfiles.ConditionType_CONDITION_TYPE_EQUAL, Boolean(false))
}

```

*Source: [go/mfilesclient/search.go](https://developer.m-files.com/APIs/gRPC-API/downloads/samples/go/mfilesclient/search.go)*

## Go and the generated code

* **Enum values carry their type's name**: `DATATYPE_TEXT` of the enum `Datatype` is
  `mfiles.Datatype_DATATYPE_TEXT`.
* **Each member of a `oneof` is a wrapper type.** A text value is
  `&mfiles.TypedValueUnion{Data: &mfiles.TypedValueUnion_Text{Text: "..."}}`. Read one with a
  type switch on `GetData()`.
* **Use the getters** (`GetObject()`, `GetVersionInfo()`, ...) to read responses: they return
  zero values instead of panicking when a message field is unset.

The helper has constructors for the common values:

```go

// Text is a single-line text value.
func Text(value string) *mfiles.TypedValue {
	return &mfiles.TypedValue{
		Type: mfiles.Datatype_DATATYPE_TEXT,
		Data: &mfiles.TypedValueUnion{Data: &mfiles.TypedValueUnion_Text{Text: value}},
	}
}

// Integer is an integer value.
func Integer(value int32) *mfiles.TypedValue {
	return &mfiles.TypedValue{
		Type: mfiles.Datatype_DATATYPE_INTEGER,
		Data: &mfiles.TypedValueUnion{Data: &mfiles.TypedValueUnion_Integer{Integer: value}},
	}
}

// Boolean is a yes/no value.
func Boolean(value bool) *mfiles.TypedValue {
	return &mfiles.TypedValue{
		Type: mfiles.Datatype_DATATYPE_BOOLEAN,
		Data: &mfiles.TypedValueUnion{Data: &mfiles.TypedValueUnion_Boolean{Boolean: value}},
	}
}

// Timestamp is a date and time value, such as "Last modified" (property 21).
func Timestamp(value time.Time) *mfiles.TypedValue {
	return &mfiles.TypedValue{
		Type: mfiles.Datatype_DATATYPE_TIMESTAMP,
		Data: &mfiles.TypedValueUnion{Data: &mfiles.TypedValueUnion_Timestamp{
			Timestamp: timestamppb.New(value),
		}},
	}
}

// Lookup is a single-select lookup value: the item with the given ID in a value list (or an
// object of the object type with that ID).
func Lookup(valueList, item int32) *mfiles.TypedValue {
	return &mfiles.TypedValue{
		Type: mfiles.Datatype_DATATYPE_LOOKUP,
		Data: &mfiles.TypedValueUnion{Data: &mfiles.TypedValueUnion_Lookup{
			Lookup: lookupItem(valueList, item),
		}},
	}
}

// MultiSelectLookup is a multi-select lookup value.
func MultiSelectLookup(valueList int32, items ...int32) *mfiles.TypedValue {
	lookups := make([]*mfiles.Lookup, 0, len(items))
	for _, item := range items {
		lookups = append(lookups, lookupItem(valueList, item))
	}
	return &mfiles.TypedValue{
		Type: mfiles.Datatype_DATATYPE_MULTI_SELECT_LOOKUP,
		Data: &mfiles.TypedValueUnion{Data: &mfiles.TypedValueUnion_MultiSelectLookup{
			MultiSelectLookup: &mfiles.MultiSelectLookup{Values: lookups},
		}},
	}
}

func lookupItem(valueList, item int32) *mfiles.Lookup {
	return &mfiles.Lookup{
		ValueListItemInfo: &mfiles.ItemInfo{ObjId: ObjID(valueList, item)},
		Version: &mfiles.ObjVerVersion{
			Type:            mfiles.ObjVerVersionType_OBJ_VER_VERSION_TYPE_LATEST,
			InternalVersion: -1,
		},
	}
}

```

*Source: [go/mfilesclient/values.go](https://developer.m-files.com/APIs/gRPC-API/downloads/samples/go/mfilesclient/values.go)*

## What the helper does

gRPC alone is not enough to talk to M-Files: part of the contract travels in call metadata, and
errors arrive in trailers. `mfilesclient` takes care of that.

**The channel.** TLS with the system's root certificates plus an optional PEM file, or no TLS at
all, and the message size limits that M-Files' own client uses: responses of any size instead of
gRPC's default 4 MiB, requests up to 48 MiB (the server's default limit). Two interceptors handle
every call:

```go

// Dial creates the channel to cfg.Address. It does not log in and does not contact the server
// yet: the first call does.
func Dial(cfg Config) (*Client, error) {
	transport, err := transportCredentials(cfg)
	if err != nil {
		return nil, err
	}
	client := &Client{host: cfg.host()}
	client.Conn, err = grpc.NewClient(cfg.Address,
		grpc.WithTransportCredentials(transport),
		grpc.WithDefaultCallOptions(
			grpc.MaxCallRecvMsgSize(MaxReceiveMessageSize),
			grpc.MaxCallSendMsgSize(MaxSendMessageSize)),
		// Every call carries the M-Files metadata; failed calls return *Error.
		grpc.WithChainUnaryInterceptor(client.unaryInterceptor),
		grpc.WithChainStreamInterceptor(client.streamInterceptor))
	if err != nil {
		return nil, err
	}
	return client, nil
}

// transportCredentials is TLS with the system's root certificates, plus cfg.CACertFile when set;
// or no TLS at all when cfg.TLS is false.
func transportCredentials(cfg Config) (credentials.TransportCredentials, error) {
	if !cfg.TLS {
		return insecure.NewCredentials(), nil
	}
	roots, err := x509.SystemCertPool()
	if err != nil {
		roots = x509.NewCertPool()
	}
	if cfg.CACertFile != "" {
		pem, err := os.ReadFile(cfg.CACertFile)
		if err != nil {
			return nil, err
		}
		if !roots.AppendCertsFromPEM(pem) {
			return nil, fmt.Errorf("%s: no PEM certificates found", cfg.CACertFile)
		}
	}
	return credentials.NewTLS(&tls.Config{RootCAs: roots, MinVersion: tls.VersionTLS12}), nil
}

```

*Source: [go/mfilesclient/client.go](https://developer.m-files.com/APIs/gRPC-API/downloads/samples/go/mfilesclient/client.go)*

**Call metadata.** Every call carries `mfiles-is-remote-call: true` and a new random 16-byte
`mfiles-activity-id-bin`; after login also the session ID in `mfiles-session-id-bin`. In Go the
value of a `-bin` key is the raw bytes in a string; gRPC base64-encodes it on the wire. M-Files
Cloud sets the cookie `mfilesmsm` to keep a client on one server instance; gRPC does not keep
cookies, so the interceptor stores `set-cookie` values and sends them back in a `cookie` header
(see [Call metadata](https://developer.m-files.com/APIs/gRPC-API/runtime/call-metadata/index.md#cookies-and-stickiness)):

```go

// withMetadata adds the M-Files call metadata to an outgoing call: the remote-call flag, a new
// activity ID, the session ID once logged in, and the cookies the server has set.
func (c *Client) withMetadata(ctx context.Context) context.Context {
	activityID := make([]byte, 16)
	_, _ = rand.Read(activityID)
	pairs := []string{headerRemoteCall, "true", headerActivityID, string(activityID)}
	if session := c.sessionID(); len(session) > 0 {
		pairs = append(pairs, headerSessionID, string(session))
	}
	if cookies := c.cookieHeader(); cookies != "" {
		pairs = append(pairs, headerCookie, cookies)
	}
	return metadata.AppendToOutgoingContext(ctx, pairs...)
}

// unaryInterceptor adds the metadata to every unary call, keeps the cookies and the metadata
// version the server returns, and turns a failure into *Error.
func (c *Client) unaryInterceptor(ctx context.Context, method string, req, reply any,
	cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
	var header, trailer metadata.MD
	opts = append(opts, grpc.Header(&header), grpc.Trailer(&trailer))
	err := invoker(c.withMetadata(ctx), method, req, reply, cc, opts...)
	c.readResponseMetadata(header, trailer)
	return decodeError(err, trailer)
}

```

*Source: [go/mfilesclient/metadata.go](https://developer.m-files.com/APIs/gRPC-API/downloads/samples/go/mfilesclient/metadata.go)*

**Login.** [LogIn](https://developer.m-files.com/APIs/gRPC-API/Reference/MFiles.IRPCLogin/LogIn/index.md) binds the session to the vault in
`login_data.vault_guid`. Its response's `session_data.session_id` (48 bytes) goes into
`mfiles-session-id-bin` from then on:

```go

// LogInWith logs in to a vault. From then on every call carries the session.
func (c *Client) LogInWith(ctx context.Context, vault string, auth *mfiles.AuthDataClient,
	clientName string) (*mfiles.SessionData, error) {
	if clientName == "" {
		clientName = filepath.Base(os.Args[0])
	}
	response, err := mfiles.NewIRPCLoginClient(c.Conn).LogIn(ctx, &mfiles.LogInRequest{
		EnvironmentData: &mfiles.EnvironmentData{ClientName: clientName, HostPlatform: "Go"},
		ClientData: &mfiles.ClientData{
			Type:     mfiles.ClientType_CLIENT_TYPE_SERVER_API,
			Language: "en",
			// A plugin (token) login uses the 32-byte form of the attempt identifier.
			Capabilities: &mfiles.ClientCapabilities{LongAuthenticationAttemptIdentifier: true},
		},
		LoginData:          &mfiles.LoginData{ServerHostname: c.host, VaultGuid: vault},
		AuthenticationData: auth,
	})
	if err != nil {
		return nil, err
	}
	session := response.GetSessionData()
	if len(session.GetSessionId()) == 0 {
		// A plugin that needs more than one round trip answers without a session.
		return nil, errors.New("LogIn returned no session: multi-step authentication is not supported")
	}
	// From now on the interceptor sends session_id in mfiles-session-id-bin on every call.
	c.mu.Lock()
	c.session = session
	c.mu.Unlock()
	return session, nil
}

```

*Source: [go/mfilesclient/client.go](https://developer.m-files.com/APIs/gRPC-API/downloads/samples/go/mfilesclient/client.go)*

With a user name and password, `authentication_data` is:

```go

// PasswordAuth is the authentication data of an M-Files user name and password.
func PasswordAuth(username, password string) *mfiles.AuthDataClient {
	return &mfiles.AuthDataClient{
		Type: mfiles.AuthDataType_AUTH_DATA_TYPE_CREDENTIALS,
		Data: &mfiles.AuthDataClientUnion{Data: &mfiles.AuthDataClientUnion_Credentials{
			Credentials: &mfiles.Credentials{
				Type:     mfiles.CredentialsType_CREDENTIALS_TYPE_MFILES,
				Username: username,
				Password: password,
			},
		}},
	}
}

```

*Source: [go/mfilesclient/client.go](https://developer.m-files.com/APIs/gRPC-API/downloads/samples/go/mfilesclient/client.go)*

**Errors.** A failed call returns `*mfilesclient.Error`, decoded from the
`mfiles-error-serialized-bin` and `mfiles-error-text-bin` trailers: the gRPC status (`Status`),
the message, the M-Files error code of the top-level error (`HResult`, such as `0x8004000B`
"Not found."; the last entry of the error stack that is not the generic `0x80040001`), every
code of the stack (`Codes`) and `Has(code)`. It keeps the gRPC status, so `status.Code(err)`
still works:

```go

// Error is a failed M-Files call. Test for it with errors.As; status.Code(err) still gives the
// gRPC status code.
type Error struct {
	Status  *status.Status         // the gRPC status: Unknown for M-Files errors
	HResult uint32                 // the M-Files error code, e.g. 0x8004000B; 0 when unknown
	Codes   []uint32               // the codes of the error stack, the original error first
	Message string                 // the error message (the gRPC status message)
	Details string                 // the long error message with the server's error stack
	Stack   []*mfiles.RPCErrorInfo // the server's error stack, the original error first
}

func (e *Error) Error() string {
	if len(e.Codes) == 0 {
		return fmt.Sprintf("gRPC error %s: %s", e.Status.Code(), e.Message)
	}
	return fmt.Sprintf("M-Files error 0x%08X (%s): %s", e.HResult, e.Status.Code(), e.Message)
}

// SignedHResult is the error code as a signed 32-bit integer, e.g. -2147221493.
func (e *Error) SignedHResult() int32 { return int32(e.HResult) }

// GRPCStatus lets status.Code and status.FromError see through the *Error.
func (e *Error) GRPCStatus() *status.Status { return e.Status }

// Has reports whether code is anywhere in the error stack.
func (e *Error) Has(code uint32) bool {
	for _, c := range e.Codes {
		if c == code {
			return true
		}
	}
	return false
}

// AsError returns the *Error in err's chain, if there is one.
func AsError(err error) (*Error, bool) {
	var mfilesErr *Error
	ok := errors.As(err, &mfilesErr)
	return mfilesErr, ok
}

// decodeError turns a failed call into *Error, using the error trailers when present.
func decodeError(err error, trailer metadata.MD) error {
	if err == nil {
		return nil
	}
	st, ok := status.FromError(err)
	if !ok || st.Code() == codes.OK {
		return err
	}
	result := &Error{Status: st, Message: st.Message()}
	if values := trailer.Get(headerErrorText); len(values) > 0 {
		result.Details = values[0]
	}
	if values := trailer.Get(headerErrorStack); len(values) > 0 {
		stack := &mfiles.RPCErrorInfoArray{}
		if proto.Unmarshal([]byte(values[0]), stack) == nil {
			result.Stack = stack.GetValue()
		}
	}
	for _, info := range result.Stack {
		result.Codes = append(result.Codes, uint32(info.GetErrorCode()))
	}
	result.HResult = errorCode(result.Codes)
	return result
}

// errorPassThrough is the generic code of an error that is only passed up the stack; it does
// not say what went wrong.
const errorPassThrough = 0x80040001

// errorCode is the code of the top-level error, the last entry of the stack: the error the
// status message describes, such as 0x800407E4 for "Access denied." Entries with the generic
// code 0x80040001 are skipped.
func errorCode(stack []uint32) uint32 {
	for i := len(stack) - 1; i >= 0; i-- {
		if stack[i] != errorPassThrough {
			return stack[i]
		}
	}
	if len(stack) > 0 {
		return stack[len(stack)-1]
	}
	return 0
}

```

*Source: [go/mfilesclient/errors.go](https://developer.m-files.com/APIs/gRPC-API/downloads/samples/go/mfilesclient/errors.go)*

Handle it like this:

```go
_, err := objects.GetObjectInfoAndProperties(ctx, request)
if mfilesErr, ok := mfilesclient.AsError(err); ok && mfilesErr.Has(mfilesclient.CodeNotFound) {
	fmt.Printf("0x%08X: %s\n", mfilesErr.HResult, mfilesErr.Message)
}
```

[Errors](https://developer.m-files.com/APIs/gRPC-API/runtime/errors/index.md) lists the common codes and what to do about each.

**The metadata structure version.** Calls that report the vault's metadata structure version return it in
the `mfiles-metadata-version-bin` trailer; `client.MetadataStructureVersion()` has the latest one. See
[Metadata structure version and caching](https://developer.m-files.com/APIs/gRPC-API/concepts/metadata-structure-version-and-caching/index.md).

## Troubleshooting

`go run ./recipes/connect` calls `QueryVersion` before it logs in, so it tells connection problems
from login problems.

| Symptom | Cause |
|---|---|
| `generate.ps1`: "No M-Files proto bundle found" | Pass `-ProtoDir` or set `MFILES_PROTO_DIR` to the folder that contains `mfiles/structs.proto`. |
| `Unavailable`, "tls: first record does not look like a TLS handshake" | TLS against a server without TLS: set `MFILES_TLS=false`, only for a lab server on a trusted network. |
| `Unimplemented`, "unexpected HTTP status code received from server: 404" | A call without TLS to a TLS endpoint, such as M-Files Cloud: remove `MFILES_TLS=false`. |
| `Unavailable`, "connection error" ... "Error while dialing" | Wrong host or port, or a firewall in between. |
| "M-Files error 0x8004001A (Unknown): Authentication failed." | Wrong user name or password, or the vault uses single sign-on (see the configuration step). |
| "M-Files error 0x80040020 (Unknown): The specified document vault does not exist." | Wrong `MFILES_VAULT`. |
| "gRPC error Unauthenticated: The current session with M-Files Server is no longer valid." | No session or an expired one: log in again. |

## Next steps

* Run the task recipes: `go run ./recipes/<name>`, with `connect`, `vault_structure`, `search`,
  `read_object`, `download_file`, `create_document`, `update_properties`, `workflow_state`,
  `paging` or `errors`. The samples' `README.md` lists their arguments; `run-tests.ps1` runs
  them all.
* The recipes explained: [Connect](https://developer.m-files.com/APIs/gRPC-API/guides/connect/index.md), [Vault structure](https://developer.m-files.com/APIs/gRPC-API/guides/vault-structure/index.md),
  [Search](https://developer.m-files.com/APIs/gRPC-API/guides/search/index.md), [Read an object](https://developer.m-files.com/APIs/gRPC-API/guides/read-object/index.md), [Download a file](https://developer.m-files.com/APIs/gRPC-API/guides/download-file/index.md),
  [Create a document](https://developer.m-files.com/APIs/gRPC-API/guides/create-document/index.md), [Update properties](https://developer.m-files.com/APIs/gRPC-API/guides/update-properties/index.md),
  [Change the workflow state](https://developer.m-files.com/APIs/gRPC-API/guides/workflow-state/index.md), [Page through results](https://developer.m-files.com/APIs/gRPC-API/guides/paging/index.md) and
  [Handle errors](https://developer.m-files.com/APIs/gRPC-API/guides/errors/index.md).
* Learn the model: [Vault and metadata structure](https://developer.m-files.com/APIs/gRPC-API/concepts/vault-and-metadata-structure/index.md),
  [Objects and versions](https://developer.m-files.com/APIs/gRPC-API/concepts/objects-and-versions/index.md),
  [Properties and values](https://developer.m-files.com/APIs/gRPC-API/concepts/properties-and-values/index.md) and
  [Search](https://developer.m-files.com/APIs/gRPC-API/concepts/search/index.md).
* Keep a session healthy in a long-running program: [Authentication and
  sessions](https://developer.m-files.com/APIs/gRPC-API/runtime/authentication/index.md).
* Find any method in the reference, starting with
  [SearchObjects](https://developer.m-files.com/APIs/gRPC-API/Reference/MFiles.IRPCSearch/SearchObjects/index.md) and
  [GetObjectInfoAndProperties](https://developer.m-files.com/APIs/gRPC-API/Reference/MFiles.IRPCObjectOperations/GetObjectInfoAndProperties/index.md).
