Run and Configure the Gateway
The shop goes from a laptop to a cluster: it needs a host name, an ingress, and a front-end
developer wants to work against routes that are not built yet. The gateway is a service the
runtime provides. An application does not write it, but it decides where it runs, which hosts it
answers on, and a handful of options. All of them live under exposition in the Context, and so do the tools for developing against the gateway:
stubs, delays and faults.
§Locally
In the application toa create writes, the gateway runs in sys,
one of the two processes ecosystem.config.js keeps under PM2:
// ecosystem.config.js
export const apps = [
// what Toa serves beside an application: its gateway, configuration and introspection
{ ...common, name: 'sys', args: 'serve configuration exposition introspection' },
// the application's components
{ ...common, name: 'app', args: `compose ${components.join(' ')}` }
] sysistoa serve configuration exposition introspection: three services of the runtime in one process.expositionis the gateway; the other two serve configuration and introspection.appistoa composewith every directory undercomponents: the application’s components as one composition.
Script of package.json | Runs |
|---|---|
npm start | pm2 start ecosystem.config.js: both processes. |
npm run sys | pm2 start ecosystem.config.js --only sys: the gateway without the components. |
npm run restart | npm run env, then pm2 restart ecosystem.config.js. Run it after any change. |
npm stop | pm2 delete ecosystem.config.js: stops both. |
The gateway listens on port 8000. With the routes of store.orders that Expose It over HTTP declares:
$ curl -i http://localhost:8000/store/orders/ The same commands arrange the processes in other ways. Run by hand, they need what the scripts
do around them: stop the two processes, which hold port 8000, and generate .env and .map.json (What a restart does):
$ npm stop
$ npm run env Running the whole application in one process starts the gateway with it:
$ npx toa mono components/* mono starts the service of every extension the components use, so a component with an exposition section brings the gateway up.
To run a composition with the gateway beside it, name the service:
$ npx toa compose components/* --service exposition --service is exact: nothing else is started. Or run the gateway as a process of its own, next
to compositions started separately:
$ npx toa serve exposition The generated Context answers on localhost. Give the authority its host in production under
the same name, and show exceptions locally:
# context.toa.yaml
exposition:
authorities:
local: localhost:8000
local@production: api.store.example
debug@local: true A key suffixed with @<environment> applies in that environment only; see Environments.
§Ports
| Port | Serves |
|---|---|
8000 | The application’s traffic. |
8004 | The gateway’s readiness probe. |
§Authorities
authorities is the only required option. It maps an identifier to each host the application
is served on. toa create named the one it wrote local; an application served on two hosts
names two:
exposition:
authorities:
local: api.store.example
outlet: api.outlet.example - The hosts are what the gateway’s ingress is created for in a deployment.
- The identifier is what
map:authorityhands to operations, and what identities and credentials are bound to. Renaming an identifier invalidates the credentials issued under it. Changing the host behind an identifier does not. - A request for a host that is not listed is still served; its authority is the host name itself.
Give each environment its own host under the same identifier, as in the example above, so that the identifier — and everything bound to it — stays stable across environments.
§In a deployment
By default the gateway is deployed on its own, with a Service and an Ingress for the
authorities’ hosts. Deploying the application is described in Deployment; these options shape the gateway’s part.
# context.toa.yaml
exposition:
authorities:
local: api.store.example
class: nginx
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: 64m
resources:
cpu: [200m, 2000m]
memory: [200Mi, 1Gi] | Option | Meaning |
|---|---|
class | The ingress class. |
annotations | Annotations of the Ingress. |
service.annotations | Annotations of the Service. |
resources | CPU and memory of the gateway’s pods: for each, the request and the limit. ~ deploys without any. |
protocol | h1 (the default) or h2c, see HTTP/2. |
Whatever stands in front of the gateway has its own limits. An ingress that caps request bodies
below the limit of an upload route, or times out idle connections
sooner than a realtime stream sends heartbeats (every 16 seconds), will cut
requests the gateway would have served. Set those through annotations.
§In a composition
The gateway can run inside a composition’s pods, instead of in pods of its own:
# context.toa.yaml
compositions:
- name: edge
components:
- store.orders
services:
- exposition Its Service and Ingress stay the same and now select that composition’s pods. Compositions
are described in Compositions.
§Readiness
The gateway answers GET /.ready on port 8004: 200 once it is ready to serve, 503 with retry-after before that. The traffic port answers 404 for /.ready. In a deployment the
probe is configured for you. Under a process manager, the gateway also sends the ready message
that PM2’s wait_ready waits for; ecosystem.config.js sets it for both processes.
§Stopping and halting
When the gateway stops, it finishes the requests in flight and ends open streams with FIN;
their readers reconnect.
While a deployment is halted, every request is answered 503 with cache-control: no-store.
§HTTP/2
The gateway serves HTTP/1.1. protocol: h2c makes it serve cleartext HTTP/2 instead:
exposition:
protocol: h2c
service:
annotations:
projectcontour.io/upstream-protocol.h2c: '8000' - The ingress controller must proxy cleartext HTTP/2 upstream, and usually needs an annotation to do so. Contour, Traefik and Envoy-based controllers can; ingress-nginx cannot.
- A gateway serving
h2canswers nothing else on that port. A controller that speaks HTTP/1.1 upstream reaches nothing. - Under
h2c, a request refused before its body was read — too large, not authorized — is answered and then cancelled. The client gets the reply, and its upload stops. A client that streams a body has to handle the cancelled stream: in Node it isERR_STREAM_PREMATURE_CLOSEon the stream it passed.
§Client address
Behind a load balancer every connection comes from the balancer, so the client’s address is never taken from the connection. The Context names the header in which the edge writes it:
exposition:
ip: cf-connecting-ip Name the header your edge overwrites: CF-Connecting-IP, X-Real-IP, X-Forwarded-For,
depending on the infrastructure. A client can send any of these itself, so only a header the
edge sets is trustworthy. Where the header holds a list, the last value — the one the edge
appended — is used.
The address is what io:throttle keys on with ip, and what failed
authentications are metered by (bouncer, see Credentials).
Without ip, or for a request without the header, a request has no address: a throttle keyed
on ip lets it through, and says so in the log.
The country is read the same way, from the header the edge writes it in:
exposition:
country: cf-ipcountry CF-IPCountry behind Cloudflare, CloudFront-Viewer-Country behind CloudFront. It is what the censor refuses by, and what map:country gives an operation. Without country, or for a request without the header, a request has no
country.
§Censor
A request can be refused by its country, before anything else looks at it:
exposition:
country: cf-ipcountry
censor: [XX, YY] A request from one of the countries censor lists is answered 451 Unavailable For Legal Reasons with an empty body, whatever its path. It is refused before
it is routed or authenticated, and calls nothing.
- The country is the request’s, read from the header
countrynames. A value matches exactly:xxis notXX. - A request of no country, or of another, is served.
- The reply carries CORS headers and preflights are answered, so a page can read the status and tell its user.
- The header is taken as it arrives. It is meaningful only if an edge in front of the gateway writes it; a client that reaches the gateway around that edge sends what it likes.
§Seeing what answered
Every reply carries an exposition header naming the gateway’s build, the Context and the
environment. When several environments or versions are in play, it tells which one a reply came
from.
debug: true puts the text of exceptions into 500 replies; see Seeing exceptions while developing. Keep it
to non-production environments: debug@local: true.
§Development directives
The dev family helps to build a client before, or without, the operations behind it.
§Stubs
dev:stub answers a method with a fixed body. No operation is called, so a route can exist
before its component does:
# context.toa.yaml
exposition:
/:
/recommendations:
anonymous: true
io:output: true
GET:
dev:stub:
- product: kettle
score: 0.93
- product: teapot
score: 0.71 $ npm run restart GET /recommendations/ HTTP/1.1
accept: application/json HTTP/1.1 200 OK
content-type: application/json
[{"product":"kettle","score":0.93},{"product":"teapot","score":0.71}] The route’s access rules and content negotiation apply as to a real reply. A stub of null answers 404, as an operation that found nothing does.
§Delays
dev:sleep lets a client ask for a slow answer, to see how its interface behaves while it
waits. The value is the longest delay the route allows, in milliseconds:
# context.toa.yaml
exposition:
/:
dev:sleep@local: 10000 A request then names the delay it wants in the sleep header, as a JSON array of the minimum
and the maximum in milliseconds; the actual delay is random between them:
GET /store/orders/ HTTP/1.1
sleep: [500, 1500] A request without the header is not delayed. A range beyond the route’s maximum is 400 Invalid sleep duration. Declared on the Context’s /, as above, it is available on every
route of the application, in the local environment only.
§Faults
dev:faulty makes a route fail at random, to see how a client copes. The value is the
probability, greater than 0 and at most 1, with which a request is answered 503 Service Unavailable:
exposition:
/:
dev:faulty@local: 0.2 dev:throw makes a method throw an exception with the given message, which answers 500:
GET:
dev:throw: Not built yet Keep dev directives out of production. Suffix them with a development environment, as in
the examples, or remove them before deploying.
§Other endpoints of the gateway
Besides the application’s routes, the gateway serves a few paths of its own.
| Path | Serves | Turned on by |
|---|---|---|
/.discovery | The tree and its page, see Describe the API. | always on |
/.rpc | Call the API as Procedures. | exposition.rpc |
/.mcp | MCP. | exposition.mcp |
/realtime/:id/ | An identity’s own stream. | always on |
/.ready on port 8004 | The readiness probe. | always on |
Identity endpoints under /identity/, and the OAuth endpoints, are described in the reference of Users and Access.
§Reference
Every key of exposition in the Context:
| Key | Type | Meaning |
|---|---|---|
authorities | { identifier: host } | Hosts the application is served on. Required. |
/ | resource tree | Routes and directives of the Context, see Routes declared in the Context. |
debug | boolean | Put exception text into 500 replies. Default false. |
class | string | Ingress class. |
annotations | { name: value } | Ingress annotations. |
service.annotations | { name: value } | Service annotations. |
resources | { cpu: [a, b], memory: [a, b] } or ~ | Resources of the gateway’s pods. |
protocol | h1 or h2c | HTTP version served. Default h1. |
ip | header name | Where the client address is read from. |
country | header name | Where the client’s country is read from. |
censor | list of countries | Requests answered 451. |
rpc | { batch } | Serve JSON-RPC, see Call the API as Procedures. |
mcp | { name, … } | Serve MCP, see Let an AI Assistant Call the API. |
bouncer | { attempts, interval } | Meter failed authentications per address; requires ip. See Credentials. |
oauth | { authorize, … } | The OAuth authorization server. See The OAuth Server. |
Related annotations of the Context, outside exposition:
| Key | See |
|---|---|
storages | Keep Files in a Storage |
realtime | Push Changes to the Browser |