Call the API as Procedures
A partner’s back end, or a script, wants to make several calls in one request and does not care for paths and verbs: one endpoint, a method name, parameters, a result. Every route of the application is also a JSON-RPC procedure, without anything being declared per route.
§Turning it on
Add rpc to the exposition of the Context, beside the authorities that toa create wrote:
# context.toa.yaml
exposition:
authorities:
local: localhost:8000
rpc: {} Every method of the tree is now also a procedure, called at POST /.rpc. The examples call the
routes of store.orders that Expose It over HTTP declares:
POST /.rpc HTTP/1.1
host: api.store.example
content-type: application/json
accept: application/json
{"jsonrpc": "2.0", "id": 1, "method": "store.orders._id.GET", "params": {"id": "4c4759e6f9c74da989d64511df42d6f4"}} HTTP/1.1 200 OK
content-type: application/json
cache-control: no-store
{"jsonrpc": "2.0", "id": 1, "result": {"id": "4c4759e6f9c74da989d64511df42d6f4", "status": "pending", "total": 4200}} This is the call GET /store/orders/4c47…d6f4/ makes, and it answers what that request
answers.
$ npm run restart
$ curl http://localhost:8000/.rpc -H 'content-type: application/json'
-d '{"jsonrpc": "2.0", "id": 1, "method": "store.orders.GET", "params": {"query": {"limit": 2}}}' Without the annotation nothing answers at /.rpc. The path is fixed and cannot be changed.
§Names
A procedure is a route and a verb, and its name is the two written as one word: the segments of
the route joined with ., a variable marked with a leading _, and the verb last.
| Route | Verb | Procedure |
|---|---|---|
/store/orders | GET | store.orders.GET |
/store/orders | POST | store.orders.POST |
/store/orders/:id | GET | store.orders._id.GET |
/store/orders/:id | PUT | store.orders._id.PUT |
/store/orders/of/:customer | GET | store.orders.of._customer.GET |
/pages/** | GET | pages.__.GET |
/ | GET | GET |
The name is not chosen: it is the address. Moving a route renames its procedure.
§Routes without a name
A segment can be part of a name if it consists of letters, digits and - only. A route with
any other segment — one containing a ., a _ or a *, or a variable whose name is not a
single word — has no procedure. It is served over HTTP all the same; /.rpc cannot call it.
Each such route is reported once in the gateway’s log when it starts:
Route cannot be addressed as a procedure route=/store/catalog/v1.0 segment=v1.0 If procedures matter to the application, name segments and variables with letters, digits and dashes only.
A route that declares map:buffer has no procedure either: it
reads the raw request, and a call is not a request.
§Parameters
params is an object, and carries what a request would carry in three places:
- a key named after a route variable is that variable;
queryis the query string, as an object;- everything else is the body.
{ "jsonrpc": "2.0", "id": 1, "method": "store.orders._id.GET",
"params": { "id": "4c4759e6f9c74da989d64511df42d6f4" } } { "jsonrpc": "2.0", "id": 2, "method": "store.orders.POST",
"params": { "customer": "b70a7dbca6b14a2eaac8a9eb4b2ff4db", "items": [], "total": 4200 } } { "jsonrpc": "2.0", "id": 3, "method": "store.orders.GET",
"params": { "query": { "criteria": "status==pending", "limit": 10 } } } - The query has a name of its own because an operation’s input may well have an
idor alimit. An operation whose input has a property namedquerycannot be given it through a procedure. - The value of a variable is one path segment and may not contain
/,?or#. - The value of
__(the rest of a path) may contain/. It reaches directives under the name**, as over HTTP.
§Results
The result is the operation’s output, restricted by the same io:output as the route’s, in the format the request’s accept asks for.
A call that found nothing answers result: null: JSON-RPC has no 404, and nothing went wrong.
A call without an id is a notification. It runs and is not answered. A request that carries
only notifications is answered 204 No Content.
§Several calls in one request
A request may carry an array of calls. The reply is an array of their answers, without entries for notifications:
[
{ "jsonrpc": "2.0", "id": 1, "method": "store.orders._id.GET",
"params": { "id": "4c4759e6f9c74da989d64511df42d6f4" } },
{ "jsonrpc": "2.0", "id": 2, "method": "store.orders.GET",
"params": { "query": { "limit": 2 } } },
{ "jsonrpc": "2.0", "method": "store.orders._id.PUT",
"params": { "id": "4c4759e6f9c74da989d64511df42d6f4" } }
] The calls run one after another, in the order given. They are separate calls: a failure of one does not undo or stop the others.
One request may carry up to 32 calls. Change the number with batch:
# context.toa.yaml
exposition:
rpc:
batch: 100 §Errors
A problem with the request as a whole is an HTTP status:
| Status | When |
|---|---|
405 | The verb is not POST. |
400 | The body is unreadable, or is an empty array. |
400 | The array is longer than batch; the body is an error with the code -32002. |
401 | A procedure requires a credential and the request presents none. |
A request has one credential for all its calls, so a missing credential refuses the request, not
a call. The 401 carries the challenge that says where to authenticate.
A problem with one call is a JSON-RPC error, in a 200 reply:
{ "jsonrpc": "2.0", "id": 4,
"error": { "code": -32001, "message": "…", "data": { "code": "NOT_PENDING" } } } | Code | Meaning |
|---|---|
-32601 | No route of that name, or no such verb on it. |
-32602 | A route variable is missing, or query is not a valid query. |
-32000 | The identity is not authorized to make this call. |
-32001 | The operation refused. Its own error code is in data.code. |
-32603 | Anything else: an exception. |
-32001 is where an application’s own errors arrive. A caller reads data.code, which is the
code the operation declares in errors: — the same value an HTTP client reads in a 422 body.
A call that is not a valid call at all — no jsonrpc version, no method — is answered with id: null.
§The same rules as HTTP
A procedure is the route it names, reached another way. It runs through the same directives.
- Authorization. A call is authorized against the path and the verb its name states. A
credential that may
GET /store/orders/:id/may callstore.orders._id.GET, and nothing more. - Read-only. A procedure named for a
GETmay only read, exactly as theGETmay; one that reaches a write fails with-32603. SeeGETdoes not write. - Input and output.
io:input,io:outputand themapdirectives that do not need a request header apply unchanged. - Replies are not cached. Every reply is
cache-control: no-store.
One difference: a route declared anonymous is reachable by a procedure call whether or not the request carries a credential. Over HTTP such a route refuses a request that presents one; the details are in Access.
§Finding the names
The answer to OPTIONS /.discovery lists every route the
caller may reach, keyed by route template. A name follows from a key mechanically: drop the
leading /, replace / with ., : with _, and append the verb.
Why this costs nothing is in Procedures and Tools.
§Summary
| Turn on | exposition.rpc: {} in the Context |
| Endpoint | POST /.rpc |
| Name | route segments joined by ., _variable, __ for **, verb last |
params | route variables by name, query, the rest is the body |
| Batch | an array of calls, 32 by default, rpc.batch |
| Application errors | -32001, code in error.data.code |