Calling actorsalpha
Call actor methods over HTTP with JSON or Candid, handle errors, and set the caller.
Every public method of an actor hosted by moxzid can be called over HTTP. This page uses this actor:
import Array "mo:core/Array";
import Runtime "mo:core/Runtime";
actor {
type Item = {
name : Text;
price : Nat;
tags : [Text];
note : ?Text;
};
type AddResult = { #ok : Nat; #err : Text };
var items : [Item] = [];
public func add(item : Item) : async AddResult {
if (item.price == 0) return #err "price must be positive";
items := Array.concat(items, [item]);
#ok(items.size())
};
public query func find(name : Text) : async ?Item {
Array.find<Item>(items, func(i) { i.name == name })
};
public query func count() : async Nat {
items.size()
};
public func addThenFail(item : Item) : async () {
items := Array.concat(items, [item]);
Runtime.trap("something went wrong");
};
public shared ({ caller }) func whoami() : async Principal {
caller
};
public query func double(n : Nat) : async Nat {
n * 2
};
};
JSON endpoints#
| Endpoint | For |
|---|---|
POST /call-json/ACTOR/METHOD | Update methods (public func) |
POST /query-json/ACTOR/METHOD | Query methods (public query func) |
The request body is a JSON array with one element per argument, [] if there are none. The response is a JSON array with one element per result. ACTOR is the name from the manifest or the actor's hex id.
curl -s -w '\n' http://127.0.0.1:7000/call-json/shop/add \
-d '[{"name": "lamp", "price": 30, "tags": ["home"], "note": null}]'
curl -s -w '\n' http://127.0.0.1:7000/call-json/shop/add \
-d '[{"name": "free lamp", "price": 0, "tags": [], "note": "promo"}]'
[{"ok":1}]
[{"err":"price must be positive"}]
curl -s -w '\n' http://127.0.0.1:7000/query-json/shop/find -d '["lamp"]'
curl -s -w '\n' http://127.0.0.1:7000/query-json/shop/find -d '["sofa"]'
[{"name":"lamp","note":null,"price":30,"tags":["home"]}]
[null]
moxzid converts between JSON and Candid using the method's Candid types, so the JSON has to match them:
| Candid type | JSON |
|---|---|
nat, int, and fixed-size integers | A number. Values beyond ±2⁵³ are returned as strings; strings are always accepted |
float32, float64 | A number, or "NaN", "Infinity", "-Infinity" |
bool | true or false |
text | A string |
principal | A string in text form, such as "2vxsx-fae" |
blob | A base64 string. An array of byte values is also accepted |
opt T | null or the value. A missing record field of type opt T is treated as null |
vec T | An array |
record { … } | An object. Unknown fields are an error |
Tuples (record { nat; text }) | An array |
variant { … } | An object with one key: {"ok": 1}, {"err": "…"}. A variant without a value is {"tag": null} |
opt opt T | {"$some": …} for the outer value, so that null and opt null stay different |
Large numbers:
curl -s -w '\n' http://127.0.0.1:7000/query-json/shop/double -d '[21]'
curl -s -w '\n' http://127.0.0.1:7000/query-json/shop/double -d '["9007199254740993"]'
[42]
["18014398509481986"]
Errors#
A request the actor can't accept gets HTTP 400 and a JSON body naming the problem:
curl -s -w '\n' http://127.0.0.1:7000/call-json/shop/add -d '[{"name": "lamp"}]'
curl -s -w '\n' http://127.0.0.1:7000/call-json/shop/nothing -d '[]'
curl -s -w '\n' http://127.0.0.1:7000/query-json/shop/add -d '[]'
{"code":400,"message":"args[0].tags: missing required field"}
{"code":400,"message":"no method \"nothing\" in the interface (it has: add, addThenFail, count, double, find, whoami)"}
{"code":400,"message":"\"add\" is an update method; use /call-json"}
If a method traps, its changes are rolled back and the call fails with 400. Here addThenFail adds an item and then traps, and the count is unchanged:
curl -s -w '\n' http://127.0.0.1:7000/call-json/shop/addThenFail \
-d '[{"name": "chair", "price": 50, "tags": [], "note": null}]'
curl -s -w '\n' http://127.0.0.1:7000/query-json/shop/count -d '[]'
{"code":400,"message":"canister trapped: something went wrong"}
[1]
| Status | Meaning |
|---|---|
200 | The method returned |
400 | Bad arguments, an unknown method, or the method trapped or rejected |
401 | Missing or wrong token |
402 | The actor has run out of cycles. Add more with POST /cycles/ACTOR |
404 | No such actor |
413 | The public actor request body is too large (8 MiB limit) |
503 | The server is still starting. Wait for /health/ready |
Setting the caller#
Calls arrive from the anonymous principal unless you set an x-caller header:
curl -s -w '\n' http://127.0.0.1:7000/call-json/shop/whoami -d '[]'
curl -s -w '\n' http://127.0.0.1:7000/call-json/shop/whoami -d '[]' \
-H 'x-caller: rrkah-fqaaa-aaaaa-aaaaq-cai'
["2vxsx-fae"]
["rrkah-fqaaa-aaaaa-aaaaq-cai"]
x-caller isn't authenticated. Any client with the server's token can claim any principal, so don't use msg.caller to protect data in actors hosted by moxzid.
OpenAPI#
GET /openapi/ACTOR returns an OpenAPI 3.1 description of the actor's JSON endpoints, generated from its Candid interface. Use it to generate a typed client, or load it into a tool such as Swagger UI.
curl -s http://127.0.0.1:7000/openapi/shop | head -n 12
{
"components": {
"schemas": {
"AddResult": {
"oneOf": [
{
"additionalProperties": false,
"properties": {
"ok": {
"oneOf": [
{
"minimum": 0,
Candid endpoints#
/call/ACTOR/METHOD and /query/ACTOR/METHOD take and return raw Candid bytes. Use them when you already have Candid encoding, for example from @dfinity/candid and the idlFactory that dfx generate writes. This example uses didc to encode and decode:
didc encode '(21 : nat)' | xxd -r -p > arg.bin
curl -s --data-binary @arg.bin http://127.0.0.1:7000/query/shop/double | xxd -p | didc decode
(42 : nat)
The body is always a complete Candid message. A method with no arguments takes the six bytes DIDL\x00\x00, not an empty body.
From JavaScript#
The JSON endpoints work with fetch, in Node 18 or later and in browsers:
const base = "http://127.0.0.1:7000";
async function call(method, args) {
const res = await fetch(`${base}/call-json/shop/${method}`, {
method: "POST",
body: JSON.stringify(args),
});
if (!res.ok) {
const { message } = await res.json();
throw new Error(`${method} failed (${res.status}): ${message}`);
}
return res.json();
}
const [added] = await call("add", [{ name: "desk", price: 120, tags: ["office"], note: null }]);
console.log(added);
try {
await call("add", [{ name: "desk" }]);
} catch (e) {
console.log(e.message);
}
node client.mjs
{ ok: 2 }
add failed (400): args[0].tags: missing required field
If the server has a token, add an Authorization: Bearer TOKEN header to each request. moxzid allows cross-origin requests from any origin, so a page served from elsewhere can call it; the token still controls access.
Limitations#
| Limitation | What to do |
|---|---|
| Updates aren't deduplicated, so a retried update can run twice | Retry an update only if it's safe to run twice |
No certificates, so @dfinity/agent's Actor class doesn't work | Use the JSON endpoints, or @dfinity/candid with the Candid endpoints |
| The caller isn't authenticated | Authenticate in front of moxzid, and pass the result as x-caller |