Errors & request IDs
Every API error carries a stable reason code and the request ID of the call that failed. Branch on the reason in code. Quote the request ID when you contact support.
What an error looks like
Errors use the Connect protocol: an HTTP status, a JSON body with a
code and a human-readable message, and a
google.rpc.ErrorInfo detail. The detail's
domain is always metalhost.net, a fixed
identifier, not a URL.
HTTP/2 401
x-request-id: 8ae81966-ffe7-4533-a5e4-ae5414427193
content-type: application/json
{
"code": "unauthenticated",
"message": "Authentication is required. Sign in, or send an API key as a Bearer token.",
"details": [{
"type": "google.rpc.ErrorInfo",
"value": "Cg9VTkFVVEhF…",
"debug": {
"reason": "UNAUTHENTICATED",
"domain": "metalhost.net",
"metadata": { "request_id": "8ae81966-ffe7-4533-a5e4-ae5414427193" }
}
}]
} reason: one of the codes below. Stable across releases.metadata.request_id: the same value as theX-Request-IDresponse header, which is on every response, successful or not.message: written for people. It may change at any time, so never match on it.
Reason codes
The aes.errors.v1.ErrorReason enum in the
API reference lists these. ErrorInfo
carries them without the ERROR_REASON_ prefix.
| Reason | Meaning |
|---|---|
INVALID_ARGUMENT | The request is not valid. ErrorInfo metadata may name the field. |
UNAUTHENTICATED | Missing, expired or revoked credentials. |
PERMISSION_DENIED | The caller's role or key scope does not allow this action. |
RESOURCE_NOT_FOUND | The resource does not exist, or was deleted. |
ALREADY_EXISTS | A resource with that name already exists. |
PRECONDITION_FAILED | Not possible in the resource's current state. |
CONFLICT | The resource changed while the request ran. Re-read and retry with the new version. |
RESOURCE_BUSY | Another operation is running on this resource. Retry when it finishes. |
RESOURCE_EXHAUSTED | A limit was reached. Retry later. |
QUOTA_EXCEEDED | A project quota limit. Request an increase. |
TRUST_TIER_LIMIT | New-account limit until the first wallet top-up settles. |
CAPACITY_UNAVAILABLE | Not enough capacity for this size right now. |
BILLING_INSUFFICIENT_BALANCE | Add funds to the wallet. Resources are prepaid. |
BILLING_SUSPENDED | The billing account is suspended. |
BILLING_NOT_CONFIGURED | Billing is not set up for this project. |
ACCOUNT_SUSPENDED | The account is suspended. Contact support. |
RATE_LIMITED | Too many requests. Back off and retry. |
UNAVAILABLE | Temporarily unavailable. Retry with backoff. |
DATACENTER_UNREACHABLE | The datacenter could not be reached. Retry with backoff. |
DEADLINE_EXCEEDED | The request took too long. Retry. |
CANCELED | The request was canceled. |
NOT_IMPLEMENTED | The action is not available yet. |
INTERNAL | A fault on our side. Retry, or open a ticket with the request ID. |
Treat an unknown reason as you would its HTTP status: new codes may be added in later releases.
Failed operations
Long-running mutations return an operations/… resource
(see Operations & idempotency).
When one ends FAILED or CANCELLED, its
failure_reason holds a code from the same list, for example
CAPACITY_UNAVAILABLE. The original request succeeded, so
it has no API error; read the operation instead.
Go SDK
SDK v1.3.0 errors are *connect.Error values. Read the
ErrorInfo detail like this:
import (
"errors"
"connectrpc.com/connect"
"google.golang.org/genproto/googleapis/rpc/errdetails"
)
// errorInfo returns the Metalhost reason and request ID of an API error.
func errorInfo(err error) (reason, requestID string) {
var ce *connect.Error
if !errors.As(err, &ce) {
return "", ""
}
for _, d := range ce.Details() {
v, derr := d.Value()
if derr != nil {
continue
}
if info, ok := v.(*errdetails.ErrorInfo); ok && info.GetDomain() == "metalhost.net" {
return info.GetReason(), info.GetMetadata()["request_id"]
}
}
return "", ""
}
_, err := client.CreateVirtualMachine(ctx, req)
switch reason, requestID := errorInfo(err); reason {
case "":
// transport failure, or no Metalhost detail
case "QUOTA_EXCEEDED", "TRUST_TIER_LIMIT":
// raise the limit, then retry
case "CAPACITY_UNAVAILABLE":
// try a smaller size
default:
log.Printf("%s (request %s): %v", reason, requestID, err)
} curl
curl -sS -X POST "$METALHOST_ENDPOINT/aes.compute.v1.ComputeService/GetVirtualMachine" \
-H "Authorization: Bearer $METALHOST_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"projects/my-app/virtual-machines/web-01"}' |
jq -r '.details[]? | select(.type == "google.rpc.ErrorInfo") | .debug | "\(.reason) \(.metadata.request_id)"' CLI
CLI v1.3.0 prints the reason, a hint for common reasons, and the request ID on stderr. Its exit codes follow the error class.
$ metalhost vm create …
Error: You've reached a quota limit. Request an increase from the Quotas page. (QUOTA_EXCEEDED)
A quota limit was reached — check `metalhost quota list` or request an increase with `metalhost quota request`.
Request ID: 7f6c2a1e-… (include it if you contact support) Asking support about an error
Include the request ID in your ticket. When you open a ticket from an
error in the dashboard, the error is attached to the ticket
automatically. API callers can pass the same details in
CreateTicketRequest.error_reference: the
error_request_id, reason, the failed
procedure and, for operations, the
operation_name.