III The API

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(' ')}` }
]
  • sys is toa serve configuration exposition introspection: three services of the runtime in one process. exposition is the gateway; the other two serve configuration and introspection.
  • app is toa compose with every directory under components: the application’s components as one composition.
Script of package.jsonRuns
npm startpm2 start ecosystem.config.js: both processes.
npm run syspm2 start ecosystem.config.js --only sys: the gateway without the components.
npm run restartnpm run env, then pm2 restart ecosystem.config.js. Run it after any change.
npm stoppm2 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

PortServes
8000The application’s traffic.
8004The 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:authority hands 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]
OptionMeaning
classThe ingress class.
annotationsAnnotations of the Ingress.
service.annotationsAnnotations of the Service.
resourcesCPU and memory of the gateway’s pods: for each, the request and the limit. ~ deploys without any.
protocolh1 (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 h2c answers 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 is ERR_STREAM_PREMATURE_CLOSE on 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 country names. A value matches exactly: xx is not XX.
  • 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.

PathServesTurned on by
/.discoveryThe tree and its page, see Describe the API.always on
/.rpcCall the API as Procedures.exposition.rpc
/.mcpMCP.exposition.mcp
/realtime/:id/An identity’s own stream.always on
/.ready on port 8004The 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:

KeyTypeMeaning
authorities{ identifier: host }Hosts the application is served on. Required.
/resource treeRoutes and directives of the Context, see Routes declared in the Context.
debugbooleanPut exception text into 500 replies. Default false.
classstringIngress 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.
protocolh1 or h2cHTTP version served. Default h1.
ipheader nameWhere the client address is read from.
countryheader nameWhere the client’s country is read from.
censorlist of countriesRequests 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:

KeySee
storagesKeep Files in a Storage
realtimePush Changes to the Browser
Search
Search the documentation