Error handling
-
CloudError— a request failed. Itsargsare(message, status, code, details):statusis the node’s HTTP status, or0when the node could not be reached or attested (codeis then"TRANSPORT");codeis the node’s error code, for exampleCONFIRM_REQUIRED;detailsis 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(aCloudError,401 UNAUTHENTICATED) — the client’s own refresh did not help, or there was no refresh token (an API key): sign in again. -
WrongNode(aCloudError,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(aCloudError,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.detailsis{"feature", "needs", "active"}(activeisnulluntil every node said which version it supports). It works once the old nodes have left the cluster; for instanceclient.apps.move(...), orclient.orgs.app_teams_scope(...)naming a package’sjoin/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), anapp_hashthat is not 32 bytes of hex, an unparseable URL. A generated model also raises it, orKeyError, for an answer missing a field the document requires (an older node): useclient.json.*there.
import jsonfrom 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))Restarts and other nodes
Section titled “Restarts and other nodes”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 UNAUTHENTICATEDto 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, isLoginRequiredraised. A request made with an API key is not refreshed. -
421 WRONG_NODE: the client moves to the next of thenodesit was given (Client(base_url, nodes=[...])) and sends the request there once;node_urlnames the node in use. With no other node, or when the next one refuses too,WrongNodeis raised with the node’sdetails. -
Unavailable: a node that cannot be connected to (status 0, code
TRANSPORT) is left for the next ofnodesby any request; a read or acallalso leaves a node that answers503or times out, a call with the same idempotency key. Each node is tried once per request, then the last error is raised.Client.connectasks 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_inandsign_upanswered421are not sent again, since they need the passkey again: the client moves to the next node and raisesWrongNode; sign in again.refreshanswered421is sent, with the same refresh token, to each next node in turn, at most once per node. Only when every node refuses isWrongNoderaised.
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])