Output and status
output: names what the route answers with: an Output type, an object
made to be sent to clients. It must be exactly what the action returns, so
the route shows its contract without anyone opening the action.
A route that succeeds answers 200, unless status: says otherwise:
POST / { input: CreateTodo output: TodoView status: SuccessStatus.created run: create_todo(input, current_principal.user)}
status: |
HTTP | Needs |
|---|---|---|
left out (SuccessStatus.ok) |
200 | output: |
SuccessStatus.created |
201 | output: |
SuccessStatus.accepted |
202 | output:; the work carries on in the background (Background work) |
SuccessStatus.no_content |
204 | no output:; the action returns Unit |
Only the output's declared fields are sent. IssuedToken and IssuedKey,
the built-in types for an issued sign-in token and a service's key
(Sign-in and identity), are sent as the objects they are: an id,
started_at and ends_at.
A route never writes a failure's status. It comes from the kind of each failure the action can produce (Failures).
Map layers
A route may also answer with a map layer: a FeatureCollection<G, P>, a
collection of shapes of geometry G, each with properties of type P
(Geospatial). It is sent bare, as the whole response,
in plain RFC 7946 GeoJSON, so a mapping library fetches it as its source.
A GET gets one from a query (Reads and writes,
"A query giving a map layer"); a route with another method gets one from
an action that builds it.
- Only a collection, only bare. A
ListorPageof layers, or a loneFeature<G, P>, isroute.output_not_output. A mapping library takes a collection as its source, and nothing asks for the others. - What
Pmay not hold is what anOutput's fields may not: a caller, a password's hash or a secret value, however deep.Pis anObject, which could hold any of them, so the route checks it: a layer whose properties hold one isroute.output_not_outputatoutput:, naming what and where. The same search looks inside a feature's properties wherever a layer appears, so anOutputwith a layer field whose properties hold a password's hash is refused at that field. - Field policy. A layer an action returns is shown to its caller, just
as an
Outputthe action builds is. So everyreturnof aFeatureCollectionin an action carries only fields every caller of the action may read. Otherwise it ispolicy.output_field_unprovedat the value returned (Field restrictions), whichever route or action calls it. - Sent as GeoJSON. The response's body is the collection as
json.writewrites it,{"type":"FeatureCollection","features":[...]}. A route test reads it back through the same codec, soresponse.outputis the layer the action returned.
Sending raw bytes
A route's body doesn't have to be JSON. sends: names the media type of a
body of raw bytes, on a route whose Output has exactly one field of a type
built on Bytes. That field is the body, with nothing around it. Here a
land registry keeps a scan of each deed, and a client opens the PDF it is
sent:
/// A scan of a deed, as a PDF.type DeedScan = Output { pdf: Bytes}
use titles/entities { Deed }use titles/types { DeedRef, DeedScan }use titles/failures { DeedNotFound }/// A deed's scan.query scan_of(deed_ref: DeedRef) fails DeedNotFound -> DeedScan { from: Deed where: Deed.deed_ref == deed_ref missing: DeedNotFound}
use titles/types { DeedRef, DeedScan }use titles/queries { scan_of }/// Scans of deeds, by reference.routes Deeds { prefix: /deeds /// A deed's scan, which never changes once it is made. GET /{deed_ref: DeedRef} { authentication: RouteAuthentication.none output: DeedScan sends: "application/pdf" cache: immutable run: scan_of(deed_ref) }}
In a route test, response.output is the Output, its field the bytes sent:
use titles/entities { Deed }use titles/types { DeedScan }use titles/actions { add_deed }test "a deed's scan is sent as it was kept" { var deed: Deed = attempt add_deed() var response: Response = request { method: HttpMethod.get path: "/deeds/{deed_ref}" params: { deed_ref: deed.deed_ref } } assert response.status == 200 assert response.output == DeedScan { pdf: hash.sha256("the scan") }}test "a reference no deed has is not found" { var response: Response = request { method: HttpMethod.get path: "/deeds/{deed_ref}" params: { deed_ref: "D999999" } } assert response.status == 404 assert response.code == "deed_not_found"}
- One field, always there. An
Outputof any other shape isroute.sends_invalid, and so is aBytes?field, aListorPage, a map layer or anAiStream. A body has no room for anything else, and nothing to send fornone. - A media type it can write in a header. A lowercase
type/subtype, with any parameters after a;, as intext/csv; charset=utf-8. JSON isn't one: JSON is what anOutput's own fields are sent as, so a route can't send a JSON body nothing declares. Nor istext/event-stream, which is what anAiStreamis sent as. - What runs it is unchanged.
output:is still exactly what the route runs returns. AGETruns a query or an action the compiler proves only reads, and any other method an action. - The response. The route's status,
Content-Typeas written,Content-LengthandX-Content-Type-Options: nosniff, so a browser takes the body as what it says it is. AHEADgets the same head, its length included, and no body. - Failures stay JSON. Everything but the success answers as any route's
does, in the failure envelope, with
Content-Type: application/json. - Not with
settle:. That isroute.sends_invalid: while the work carries on,settle:answers with how far it has got, which raw bytes can't say. - A repeat gets the same bytes. Under an
Idempotency-Key, a route of another method keeps what it sent as any route does, to the byte. - The field is read as any field is. Policy and
personal:marks apply whichever way it is sent: a field not every caller may read ispolicy.output_field_unproved, and a read of a marked field counts as one (Field restrictions, Marking personal fields). Bodies are never logged. - OpenAPI lists the success under the media type, with no schema, as
OpenAPI 3.1 writes a binary body.
x-jadpo-sendsnames theOutputand the field, andContent-Lengthis one of itsheaders. Failures are listed as any route's are. An importer gives the operation nooutput:, since it reads only JSON. - The API record says
sends application/pdfunder the route, and theOutputkeeps its entry. A route that starts or stops sending raw bytes, or sends another media type, breaks whoever reads it, so it needs an acceptance:ApiChange.media_type_changed(Breaking changes).
Answers that never change
Nothing caches a route's answer unless the route says so. cache: immutable
says the answer never changes, as a scan of a signed deed doesn't.
The route's 200 then carries Cache-Control: public, max-age=31536000, immutable, so a browser or a CDN keeps it for a year and doesn't ask again. It
goes with sends: or with JSON. A failure never carries the header, so a 404
for a deed not scanned yet isn't kept.
Anything else is route.cache_invalid:
- Only
immutable. It is the one thingcache:says. - Only a
GET. No other method's answer can be kept. - Only a route anyone may call. A public cache would hand what one caller was sent to anyone who asked, so a route needing a signed-in caller can't say it.
- Only an answer of 200. Any other
status:is refused. - No personal data. No erasure reaches a public cache, so what the route
runs can't give any, directly or through what it calls, and its
output:can't say it holds any. It counts as the personal-data report counts: a field markedpersonal:, and a standard type that is personal data whatever holds it, such asPhoneNumberorEmail, on a field with no mark as much as on its own (What the compiler checks).
OpenAPI says x-jadpo-cache: immutable on the operation and lists the header
on the 200, and the API record says cache immutable under the route, as the
record below shows. Adding it or taking it away breaks no client, so neither
needs an acceptance.
// Generated by Jadpo from the routes of API main; update it with `jadpo api update`, never by hand.record api 1GET /deeds/{deed_ref: Text { pattern: "^D[0-9]{6}$" }} authentication none output DeedScan sends application/pdf status 200 cache immutable fails deed_not_foundGET /openapi.json generated openapi authentication none status 200output DeedScan pdf: Bytesfailure deed_not_found status 404 message "No deed has this reference."
Markdown for agents: /docs/output-and-status.md.Built from Jadpo main at aee2e8f.