III The API

What a Client Sends and Receives

Someone writes the shop’s web or mobile client. They ask which formats the API speaks, which statuses it answers, what an error looks like and what a page on another origin may do. None of it is declared per route.

§Formats

Request and response bodies are encoded in any of:

Media typeFormat
application/jsonJSON
application/yamlYAML
application/msgpackMessagePack
text/plainPlain text

A request may also be sent as application/x-www-form-urlencoded. A name repeated in a form is read as a list. Forms are only read: a response is never written as one.

The request says what it sends with content-type and what it wants back with accept. The two are independent. The route is POST /store/orders/, which Expose It over HTTP declares:

POST /store/orders/ HTTP/1.1
content-type: application/json
accept: application/yaml

{"customer": "b70a7dbca6b14a2eaac8a9eb4b2ff4db", "items": [], "total": 4200}
HTTP/1.1 201 Created
content-type: application/yaml
vary: accept

id: 99988d785d7d445cad45dbf8531f560b

A charset parameter on content-type is accepted: application/json; charset=utf-8.

Without accept, the reply is JSON. An accept that names none of the formats above is 406 Not Acceptable. Errors are encoded by the same negotiation as successful replies.

$ curl -i http://localhost:8000/store/orders/ -H 'accept: application/yaml'

Routes that move bytes instead of values — stored files and streamed bodies — set their own media types; see Upload and Download Files and Stream Through the API.

§Statuses

The gateway answers with these statuses, each meaning what HTTP defines.

StatusWhen
200 OKThe operation answered.
201 CreatedThe operation answered a POST.
202 AcceptedA workflow runs on a stored file, see Process an Uploaded File.
204 No ContentThe operation answered and the route returns no body.
206 Partial ContentA range of a stored file.
304 Not ModifiedThe client already holds this reply, see Cache Replies and Prevent Lost Updates.
400 Bad RequestThe body, the query string or a header does not fit what the route takes.
401 UnauthorizedNo credential, or one that is not valid.
403 ForbiddenThe caller may not reach this method.
404 Not FoundNo such route, or nothing found behind it.
405 Method Not AllowedThe route does not declare this method.
406 Not Acceptableaccept names nothing the route can answer in.
409 ConflictThe entity changed concurrently, or already exists.
410 GoneA stream token the storage can no longer continue from.
412 Precondition FailedThe version in if-match is not the current one.
413 Content Too LargeThe body is larger than the route takes.
415 Unsupported Media TypeThe body’s media type is not one the route takes.
422 Unprocessable EntityThe request is valid, and the operation refused it.
429 Too Many RequestsThrottled, see io:throttle.
451 Unavailable For Legal ReasonsRefused by the censor.
500 Internal Server ErrorAn exception.
501 Not ImplementedAn HTTP method the gateway does not serve.
503 Service UnavailableThe deployment is halted or on its way there.
504 Gateway TimeoutThe process a route named did not answer in time.

401 and 403 are the subject of Access.

§Errors

There are three kinds of failure, and they look different to a client.

The operation refused. It returned one of the errors it declares (Refuse a Request). The status is 422 and the body is the error’s code, with its cause where the operation gave one:

HTTP/1.1 422 Unprocessable Entity
content-type: application/yaml

code: NOT_PENDING

A client branches on code. These codes are part of the application’s contract and are listed for each method by OPTIONS.

The request does not fit. The gateway or the runtime refused it before the operation ran. The status is in the 4xx range and the body is a sentence saying what is wrong:

HTTP/1.1 400 Bad Request
content-type: application/json

"must have required property 'customer'"

These messages are for the developer of the client. They are not codes, and a client should branch on the status.

Something broke. An exception in an operation, or a dependency that did not answer. The status is 500 and the body is empty; the exception is in the application’s logs, under the trace the ray header names.

§Seeing exceptions while developing

With debug on, the body of a 500 carries the exception. It is one more key of exposition in the Context, read at the next npm run restart:

# context.toa.yaml
exposition:
  authorities:
    local: localhost:8000
  debug@local: true
HTTP/1.1 500 Internal Server Error

Error: Cannot read properties of undefined (reading 'balance')

Do not turn debug on in production: an exception’s text may show what the application holds. The @local suffix limits the option to the local environment; see Environments.

§Headers on every reply

HeaderValue
rayThe trace ID of the request.
server-timingHow long the stages of processing took.
expositionThe gateway’s build, the Context and the environment that answered.
varyWhat the reply depends on: origin, accept, and any header a route maps.

§ray and tracing

Every request is processed within a trace, and every log entry it causes anywhere in the application carries the same trace ID. The reply returns that ID:

HTTP/1.1 201 Created
ray: 0af7651916cd43dd8448eb211c80319c

Show it to the user beside an error message, or attach it to a support ticket: it finds the request in the logs.

A client can supply the trace to continue. Two request headers are read:

  • traceparent — the W3C Trace Context header that instrumented clients and proxies send. The client’s span becomes the parent of the gateway’s.

    traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
  • ray — a bare trace ID, 32 lowercase hex digits, for a client without instrumentation that wants to correlate its requests with the server’s traces.

    ray: 4bf92f3577b34da6a3ce929d0e0e4736

If both are present, traceparent is used. A header with an invalid value is ignored and a new trace starts. Traces are set up in Traces.

§Multipart responses

A reply that is a stream of values is sent as a multipart response: one part per value, written as it becomes available. The media type is a multipart/ subtype chosen by the same content negotiation, once for the whole response:

acceptcontent-typeEach part is
application/jsonmultipart/jsonJSON
application/yamlmultipart/yamlYAML
application/msgpackmultipart/msgpackMessagePack
text/plainmultipart/textPlain text

/store/orders/feed/ here is a collection stream:

GET /store/orders/feed/ HTTP/1.1
accept: application/json
HTTP/1.1 200 OK
content-type: multipart/json; boundary=cut

--cut

"ACK"
--cut

{"entry":{"id":"4c4759e6f9c74da989d64511df42d6f4","status":"pending","total":4200}}
--cut

{"token":"eyJ2IjoxLCJwIjoi..."}
--cut

"FIN"
--cut--

Rules for the code that reads one:

  • The boundary is always cut. Parts carry no headers of their own.
  • The first part is the text ACK, sent as soon as the response starts.
  • The last part is the text FIN.
  • Do not take FIN for success. A stream that fails after it started, or that is still open when the gateway stops, also ends with FIN: the status was already sent. A stream that must be read whole carries its own end marker — a collection stream ends with a token.
  • Some streams send a text part heartbeat <time> to tell a quiet stream from a lost one.

The kinds of streams are in Stream Through the API.

§Browsers on other origins

The gateway answers CORS requests from any origin. Nothing is configured.

A preflight is answered for every path:

OPTIONS /store/orders/ HTTP/1.1
origin: https://store.example
access-control-request-method: POST
HTTP/1.1 204 No Content
access-control-allow-origin: *
access-control-allow-methods: GET, POST, PUT, PATCH, DELETE, LOCK, UNLOCK, OPTIONS
access-control-allow-headers: *, authorization
access-control-max-age: 86400

A page may send any request header and read every response header.

A preflight permits everything, not what it asked about, so the browser sends one per URL and reuses it for whatever method and headers the page sends there next, for up to a day. The browser keeps a preflight per URL, the query included, so a request to another URL is preflighted again.

Send requests in the default credentials mode. A browser refuses these permissions for a request sent with credentials: 'include', or an XMLHttpRequest with withCredentials.

Every reply of the gateway carries the CORS headers, errors included, so a page can read the status and body of any failure the application answered. A reply does not depend on the origin of its request, so a cache keeps one copy of it for every origin.

An OPTIONS request without access-control-request-method is not a preflight. It is introspection, and a page may send it.

There are no cookies. A credential travels in the authorization header.

§When the deployment is halted

A halted deployment answers every request, preflights included, with 503:

HTTP/1.1 503 Service Unavailable
cache-control: no-store
retry-after: 120

retry-after says when it is back, where that is known. A client should treat 503 as “try later”, not as an error of the request.

§A client’s checklist

  • Send a trailing slash on every path except those of stored files.
  • Send accept; expect JSON otherwise.
  • Branch on the status, and for 422 on code.
  • On 429 and 503, wait for retry-after seconds.
  • On 412, read the entity again and reapply the change.
  • Keep ray of failed requests.
  • For a multipart response, read until FIN and do not take FIN for success.
Search
Search the documentation