Pass Headers, Language and Host to an Operation
The catalogue shows prices in the visitor’s currency and titles in their language; a payment
provider’s callback has to be verified by its signature header. An operation receives the body
as its input and the path as its query, and knows nothing of HTTP. The map directives put the
rest of a request — headers, the language, the host, the claims of a token — into input
properties, so the operation gets them as plain arguments. The require directives state what
a request must carry.
§A header as an input property
The catalogue is a component of its own, store.catalog; browse here is its
operation that lists products, with an input of currency, language and storefront.
# components/store.catalog/manifest.toa.yaml
exposition:
/:
io:output: true
GET:
endpoint: browse
map:headers:
currency: x-currency $ curl http://localhost:8000/store/catalog/ -H 'x-currency: EUR' browse receives input.currency set to EUR. The rules are the same for every map directive:
- The operation’s input must be an object.
- A mapped property overwrites one the client sent. Whatever is mapped is the route’s to fill, not the caller’s.
- A mapped property is not subject to
io:input. A route may forbid every body property and still fill some itself. - In the answer to
OPTIONSa mapped property is taken out of the input schema, because a client has nowhere to send it.
§Headers
map:headers maps input properties to request headers. Keys are input properties, values are
header names:
map:headers:
currency: x-currency
agent: user-agent A header sent several times arrives as one comma-separated value. A header that is absent leaves
the property unset; make the header mandatory with require:header, or the
property optional in the operation’s input.
Each mapped header is added to the reply’s vary. A browser on another origin may send any of
them (CORS).
§Route variables
Route variables normally go to the query (or, for POST, to the input) on their own, see Map Methods to Operations. map:segments puts a variable into
the input under a name of your choice. Keys are input properties, values are variable names. amend here is a transition of store.orders that changes one item
of an order and takes the product in its input:
# components/store.orders/manifest.toa.yaml
exposition:
/:id/items/:product:
io:output: true
PUT:
endpoint: amend
map:segments:
product: product PUT /store/orders/4c47…d6f4/items/kettle/ calls amend on the order 4c47…d6f4 with input.product set to kettle.
A variable that is mapped this way is still used for criteria and still seen by other
directives. Prefix its name with ~ to take it out of both:
map:segments:
product: ~product Use ~ when the variable is an argument and not a property of the entity. Without it, product==kettle would be added to the criteria, and an order has no such property.
The rest of a path matched by ** is mapped under the name '**'. pages here is a
component whose render answers the page at input.path:
# components/pages/manifest.toa.yaml
exposition:
/**:
io:output: true
GET:
endpoint: render
map:segments:
path: '**' GET /pages/help/shipping/ calls render with input.path set to help/shipping.
§Language
map:languages lists the languages the application supports, and map:language puts the one
that best matches the request’s accept-language into an input property:
# components/store.catalog/manifest.toa.yaml
exposition:
/:
map:languages: [en, fr]
io:output: true
GET:
endpoint: browse
map:language: language GET /store/catalog/ HTTP/1.1
accept-language: fr-CA, fr;q=0.9, en;q=0.5 HTTP/1.1 200 OK
content-language: fr
vary: origin, accept-language, accept browse receives input.language set to fr. If none of the supported languages matches, the
first of the list is used. The reply carries content-language and varies on accept-language.
The list is usually the same for the whole application. Declare it once, in the Context, with its shortcut:
# context.toa.yaml
exposition:
authorities:
local: localhost:8000
/:
languages: [en, fr] §Client address and country
map:ip and map:country put the client’s address and country into an input property, as the
gateway reads them from the headers ip and country name:
# components/store.catalog/manifest.toa.yaml
exposition:
/:
POST:
endpoint: record
map:ip: ip
map:country: country The property is null where the request has none, and is set on every call, so a body that
names it is overwritten rather than believed.
§The host
An application may be served on several hosts. The Context gives each one an identifier, an authority. toa create wrote one, local: localhost:8000; a shop served on two hosts names
both instead:
# context.toa.yaml
exposition:
authorities:
shop: api.store.example
outlet: api.outlet.example map:authority puts the identifier of the host a request was sent to into an input property:
# components/store.catalog/manifest.toa.yaml
exposition:
/:
io:output: true
GET:
endpoint: browse
map:authority: storefront A request to api.outlet.example calls browse with input.storefront set to outlet. This
is how one deployment serves several storefronts with different catalogues.
A host that is not among the authorities is passed as it is, lowercased: api.unknown.example. A host that is not a host name (with an optional port) is answered 400.
An identity and its credentials belong to one authority, so renaming an identifier invalidates them; see Authorities. Hosts are given per environment in Run and Configure the Gateway.
§Token claims
map:claims maps the claims of the request’s Bearer token to input properties. Keys are input
properties, values are claim names. accounts is the component of customers that Sign Users Up and In writes; open here is an operation of it that
opens an account for the holder of the token:
# components/accounts/manifest.toa.yaml
exposition:
/:
POST:
endpoint: open
map:claims:
email: email
verified: email_verified If the request is not authenticated with a Bearer token, or the token has no such claim, the
property is not set. Which tokens are accepted is described in Credentials.
§The raw body
map:buffer hands the body to the operation as it was sent, unparsed. The value is the input
property that receives it. payments.webhooks is the component that receives what a payment
provider sends, and payments.gateway.decode the operation that checks the provider’s signature; Receiving Webhooks writes both:
# components/payments.webhooks/manifest.toa.yaml
exposition:
/:
POST:
anonymous: true
map:buffer: payload
map:headers:
signature: x-signature
io:output: false
endpoint: ingest // components/payments.webhooks/operations/ingest.ts
import type { Context, IngestInput, State } from '../types/index.d.ts'
export async function transition (input: IngestInput, entry: State, context: Context) {
const notification = await context.remote.payments.gateway.decode({
input: { payload: input.payload, signature: input.signature ?? '' }
})
if (notification instanceof Error) return notification
// ...
} This is what a webhook needs: a signature is computed over the exact bytes, which parsing and re-encoding would not preserve.
- The body arrives as a UTF-8 string.
- It is not parsed, so the request may carry any
content-type. - The route receives no parsed body: the input holds the buffer and whatever else is mapped.
- A route with
map:bufferis served over HTTP only. It is not available as a procedure.
§The body as a stream
map:stream hands the body to the operation as a stream, for content that has to flow through
an operation without being held in memory. It is described in Stream Through the API. A route cannot declare both map:stream and map:buffer.
§The process
map:instance names the route variable that carries the process a stateful operation is called
on. It is described in Stream Through the API.
§Requiring headers
require:header refuses a request that lacks a header. require:headers takes a list:
# components/store.orders/manifest.toa.yaml
exposition:
/:id:
require:header: if-match
io:output: [id, status]
PUT: approve PUT /store/orders/4c4759e6f9c74da989d64511df42d6f4/ HTTP/1.1
accept: text/plain HTTP/1.1 400 Bad Request
Header required: if-match Requiring if-match makes every write to the route conditional on the version the client read,
so no client can overwrite a change it has not seen; see Prevent lost updates.
require:headers: [if-match, x-currency] Like every directive, a requirement on a node applies to all methods under it.
§Summary
| Directive | Value | Fills the property with |
|---|---|---|
map:headers | { property: header } | The header’s value. |
map:segments | { property: variable } | The route variable; ~variable also removes it from criteria. |
map:languages | list of language codes | Nothing: declares the supported languages. |
map:language | property | The best supported language for accept-language. |
map:authority | property | The identifier of the host. |
map:ip | property | The client address, or null. |
map:country | property | The client’s country, or null. |
map:claims | { property: claim } | The claim of the Bearer token. |
map:buffer | property | The unparsed body, as a string. |
map:stream | property, or an object | The body as a stream. |
map:instance | variable | Nothing: names the process to call. |
require:header | header | Nothing: refuses a request without it. |
require:headers | list of headers | Nothing: refuses a request that lacks one of them. |