Choose What a Route Accepts and Returns
An order holds properties a customer must not set, such as status, and may hold ones a customer
must not see. A route is usually narrower than the operation behind it. The io directives state
what a route lets out of a reply, what it accepts in a body, which status a reply carries, and how
often it may be called.
The orders are those of store.orders: Create a Component writes the
component with its create and approve, and Expose It over HTTP its
routes, with io:output: true and anonymous: true on /. The fragments here show the io directives only; a route still needs its access rule.
§Choose what comes back
A method returns nothing unless it says what it returns. Without io:output the operation
runs and its reply is dropped:
# components/store.orders/manifest.toa.yaml
exposition:
/:id:
GET: observe GET /store/orders/4c4759e6f9c74da989d64511df42d6f4/ HTTP/1.1 HTTP/1.1 204 No Content So a property added to an entity later never leaks through a route written before it existed.
§Naming the properties
io:output lists the properties of the reply the client receives:
exposition:
/:id:
io:output: [id, status, total]
GET: observe HTTP/1.1 200 OK
content-type: application/yaml
id: 4c4759e6f9c74da989d64511df42d6f4
status: pending
total: 4200 The list applies to each object of the reply, whatever its shape: the object itself, each object of an array, each object a stream yields. A value that is not an object — a number, a string — is returned as it is.
System properties are properties like any other. A route whose clients need the version or the
creation time lists them: io:output: [id, status, VERSION, CREATED].
§Returning everything
true returns the reply whole:
# components/store.catalog/manifest.toa.yaml
exposition:
/quote:
GET:
endpoint: quote # a computation that answers a number
io:output: true Use it where the operation does not return an object with known properties: a primitive, a stream of bytes, an object whose keys are data. For entities, prefer a list.
§Returning nothing
false says the same as declaring nothing, on purpose. cancel here is a transition that sets
the order’s status to cancelled, as in Write Operations:
# components/store.orders/manifest.toa.yaml
exposition:
/:id:
DELETE:
endpoint: cancel
io:output: false A method with no output takes the status an empty body takes: 204, 201 for a POST, and 404 where the operation found nothing. A stream the operation answers is closed unread.
§Output on a node and on a method
io:output on a node applies to every method below it. Where a method states one too, the
client receives only what both permit:
exposition:
/:
io:output: [id, status, total]
/:id:
GET:
io:output: [status, total, items]
endpoint: observe GET /store/orders/:id/ returns status and total. items is not in the node’s list, so
the method cannot add it. A node’s list is therefore the ceiling for everything under it; to
return more on one route, declare the route outside the node or widen the node’s list.
§What is not restricted
A returned error. The body of a 422 is the code and the cause of the error the operation declared,
not its output, and no list applies to it.
§Choose what a client may send
By default a route accepts whatever the operation’s input schema accepts. io:input lists the
properties a client may send:
# components/store.orders/manifest.toa.yaml
exposition:
/:
anonymous: true
io:input: [customer, items, total]
io:output: [id]
POST: create POST /store/orders/ HTTP/1.1
content-type: application/yaml
accept: text/plain
customer: b70a7dbca6b14a2eaac8a9eb4b2ff4db
items:
- product: kettle
quantity: 1
total: 4200
status: approved HTTP/1.1 400 Bad Request
Unexpected input: status Use it where an operation’s input holds properties that are not the client’s to set. The same operation can then be exposed twice, to a customer with a narrow input and to staff with a wider one.
- The list must be a subset of the operation’s input properties.
- With
io:inputdeclared, a body that is not an object is refused with400. It therefore fits operations whose input is an object or an array of objects. io:input: []accepts no body properties at all. Properties the route fills itself withmapdirectives are not affected: they are the route’s, not the client’s.
Both directives in one scenario. Its first two steps are among those the application came with (Write a Test); the third is one more, for a line a response must not have:
// features/steps/http.ts
Then('the response does not contain:', function (this: World, text: string) {
this.agent.responseExcludes(text)
}) # features/orders.feature
Scenario: A customer cannot set the status and receives only the id
When the request is sent:
"""
POST /store/orders/ HTTP/1.1
content-type: application/yaml
accept: application/yaml
customer: b70a7dbca6b14a2eaac8a9eb4b2ff4db
items: []
total: 4200
status: approved
"""
Then the response is received:
"""
400 Bad Request
"""
When the request is sent:
"""
POST /store/orders/ HTTP/1.1
content-type: application/yaml
accept: application/yaml
customer: b70a7dbca6b14a2eaac8a9eb4b2ff4db
items: []
total: 4200
"""
Then the response is received:
"""
201 Created
id: ${{ order.id }}
"""
And the response does not contain:
"""
status:
""" $ npm run sys
$ npm run env
$ npm run features §Choose the status
The status of a response follows from the reply: 200, 201 for a POST, 422 for a
returned error. Some protocols need more — an endpoint that must answer 400 with a
structured body of the protocol’s own, or 200 where a POST would say 201.
io:status names a property of the output that carries the status. The property is removed
from the body:
# components/payments.webhooks/manifest.toa.yaml
exposition:
/callbacks:
POST:
endpoint: accept
io:status: status
io:output: [status, received, error] // components/payments.webhooks/operations/accept.ts
import type { AcceptInput, Context } from '../types/index.d.ts'
export async function effect (input: AcceptInput, context: Context) {
const order = await context.remote.store.orders.observe({ query: { id: input.order } })
if (order === null) return { status: 400, error: 'unknown_order' }
return { status: 200, received: true }
} HTTP/1.1 400 Bad Request
content-type: application/yaml
error: unknown_order payments.webhooks is the component that receives what a payment provider sends, written in Receiving Webhooks; accept here is an operation of it for
a provider whose protocol expects 200 or 400 with a body of its own.
- The method lists the status property in
io:output: the directive reads it from what the route lets out. - A reply without the property is answered with the status it would have had.
- A property that is not a number is an exception.
- An operation that uses
io:statusfor failures returns them as output, not as errors:return { status: 400, error: 'invalid_redirect_uri' }.io:outputthen lists the properties of both shapes.
Reach for it only when a protocol dictates the status. For an application’s own refusals,
return an error and let the client read 422 and the code.
§Limit how often a route is called
io:throttle limits how often a route is called:
exposition:
/:
io:throttle:
key: ip
requests: 60
interval: 60
POST: create Requests are metered per key. A key may spend requests at once and earns them back at requests per interval seconds. A request whose key has nothing left is refused:
HTTP/1.1 429 Too Many Requests
retry-after: 1 retry-after is the number of seconds until one request would be admitted again. There is no
lockout: a client that has overspent is admitted as soon as it has earned one request back, and
asking in the meantime costs it nothing.
So requests is the burst, and requests / interval is the sustained rate. The example allows
sixty orders at once from one address, and one a second after that.
§Keys
key is one of the following, or a list of them. A list meters the combination: [route, ip] gives each address its own budget on each route.
| Key | Metered against |
|---|---|
ip | The client address. |
path | The path of the request: /store/orders/4c47…d6f4/. |
route | The route as declared: /store/orders/:id. All paths of the route share a budget. |
identity | The authenticated identity. Requests without one share a single budget. |
segment: <name> | The value of a route variable. |
io:throttle:
key:
- route
- identity
requests: 10
interval: 60 Choose route over path for routes with variables: keyed on path, walking through
identifiers is a way around the limit.
Pair identity with ip where anonymous requests reach the route, or they all spend one
budget.
ip needs the gateway to know the client’s address, which behind a load balancer it reads from
a header the Context names; see Client address. A request with
no known address is not throttled by ip.
§Charging only for some responses
A condition meters only the requests whose response matches. A request that does not match is
served and costs nothing:
exposition:
/:
io:throttle:
key: ip
condition:
status: 404
requests: 20
interval: 600 An address may probe for twenty missing orders at once and one every thirty seconds thereafter. Requests that find something are never counted.
A condition is checked against the response, so it decides what is charged, not what is refused:
the request that exhausts a budget is still served, and the next one is refused. status is the
only condition.
§How exact it is
The budget is for the whole application, not per gateway replica, and it is approximate: a burst spread over several replicas can overshoot it briefly. Use throttling to stop abuse and runaway clients, not to enforce an exact quota.
§Reference
| Key | Type | Meaning |
|---|---|---|
key | key or list of keys | What the budget is kept for. Required. |
requests | integer ≥ 1 | The burst. Required. |
interval | integer ≥ 1 | Seconds in which requests are earned back. Required. |
condition | { status } or a list of them | Charge only matching responses. |
§Read-only calls
io:readonly states whether the call a method makes may change state. It exists for the rare GET that has to write and is described in Map Methods to Operations.
§What clients are told
The io directives shape what a route says about itself. In the answer to OPTIONS, the input schema is the operation’s restricted
by io:input, the output schema is restricted by io:output, and a method without io:output states no output at all.
§Summary
| Directive | Value | Default |
|---|---|---|
io:output | list of properties, true or false | false: no body |
io:input | list of properties | everything the operation takes |
io:status | name of an output property | status follows from the reply |
io:readonly | boolean | true for GET and HEAD, false otherwise |
io:throttle | { key, requests, interval, condition } | not throttled |