Skip to content

Error handling

  • CloudError — a request failed. Its args are (message, status, code, details):

    • status is the node’s HTTP status, or 0 when the node could not be reached or attested (code is then "TRANSPORT");
    • code is the node’s error code, for example CONFIRM_REQUIRED;
    • details is a JSON string ("null" when there are none).

    Every way of calling the node raises it the same way: the generated methods (client.apps.call(...)), client.json.*, the calls on any path and the hand-written flows. The method reference lists the codes core’s document names for each method.

  • LoginRequired (a CloudError, 401 UNAUTHENTICATED) — the client’s own refresh did not help, or there was no refresh token (an API key): sign in again.

  • WrongNode (a CloudError, 421 WRONG_NODE) — the node runs a mero-tee release the account’s owners have not approved, and the client knows no other node that took the request: connect to another node of the cluster.

  • FeatureNotActive (a CloudError, 409 FEATURE_NOT_ACTIVE) — the command, or a field of it, needs a cluster version not every node of the cluster supports yet: a rollout from an older release is under way, and nothing was done. details is {"feature", "needs", "active"} (active is null until every node said which version it supports). It works once the old nodes have left the cluster; for instance client.apps.move(...), or client.orgs.app_teams_scope(...) naming a package’s join/keep, on a cluster still holding a node from before 1.0.0-rc.15.

  • ValueError — an argument caught before sending: a body the document does not allow (a value outside an enum, a missing required field), an app_hash that is not 32 bytes of hex, an unparseable URL. A generated model also raises it, or KeyError, for an answer missing a field the document requires (an older node): use client.json.* there.

import json
from calimero_client_py import CloudError
try:
client.apps.call(app=app, method="clear")
except CloudError as e:
message, status, code, details = e.args
if status == 0:
print("node unreachable or not attested:", message)
else:
print(status, code, json.loads(details))

A session token is signed by the node that issued it. Other nodes take it only while they can vouch for that node’s signing key, so when the issuer is replaced, removed or out of reach (a rolling deploy, an image rollout), they answer 401 — not because the user signed out. The client answers that itself:

  • 401 UNAUTHENTICATED to a request made with a session: it refreshes once (POST /auth/refresh, which any node takes) and sends the request again. Requests refused at the same time share that one refresh: a refresh token is used once, and a second use revokes the session. Only when the refresh is refused, or the request is refused again, is LoginRequired raised. A request made with an API key is not refreshed.

  • 421 WRONG_NODE: the client moves to the next of the nodes it was given (Client(base_url, nodes=[...])) and sends the request there once; node_url names the node in use. With no other node, or when the next one refuses too, WrongNode is raised with the node’s details.

  • Unavailable: a node that cannot be connected to (status 0, code TRANSPORT) is left for the next of nodes by any request; a read or a call also leaves a node that answers 503 or times out, a call with the same idempotency key. Each node is tried once per request, then the last error is raised. Client.connect asks the node directory for the cluster’s nodes again after that.

Sending a request again is safe, a call too: the node answers 401 and 421 while it judges the credential, before it runs anything. A node also answers 421 to the ceremonies that mint a session before it spends their passkey challenge or refresh token, and:

  • sign_in and sign_up answered 421 are not sent again, since they need the passkey again: the client moves to the next node and raises WrongNode; sign in again.
  • refresh answered 421 is sent, with the same refresh token, to each next node in turn, at most once per node. Only when every node refuses is WrongNode raised.
from calimero_client_py import Client, LoginRequired, WrongNode
client = Client(urls[0], nodes=urls[1:], tee_profiles=["..."])
try:
client.apps.call(app=app, method="set", args={"key": "a"})
except LoginRequired:
client.sign_in(passkey)
except WrongNode as e:
print("no node of the cluster here serves this account:", e.args[3])