Qirava DMS docs
Response envelope: {error|data|root}
Every response the Qirava DMS returns over HTTP (and WebSocket) uses one uniform shape. Whether a call succeeds or fails, the JSON body always has three top-level keys — error, data, and root — and exactly one of error/data is null. This means a client only ever writes one parser: check error; if it is null, read data; either way, read root for metadata. This page documents the envelope field by field, the mapping from internal response codes to stable error strings and HTTP statuses, the shape data takes for each output kind, and the took_us / request_id metadata in root. It is transcribed directly from src/workers/envelope.rs and src/functions/db/engine.rs.
The three keys#
Every envelope is a JSON object with exactly these top-level keys. error and data are mutually exclusive: a successful response carries data (with error: null); a failed response carries error (with data: null). root is always present and carries response metadata.
- error
- On failure: an object
{ "code": <stable_code>, "message": <string> }. On success:null. - data
- On success: the function result — rows, a record, a value, an HTML/text string, or a JSON fragment, depending on the route's output kind. On failure:
null. - root
- Always present. Metadata about the call. Its exact fields depend on the path that produced the envelope (see "Two builders, two root shapes" below).
Success envelope#
When a function returns ResponseCode::Ok, the worker builds the success envelope. error is null; data holds the serialized result; root carries the timing and request id. The HTTP status is 200.
Request
POST /api/qql HTTP/1.1
Host: localhost
Content-Type: text/plain
SELECT name FROM users LIMIT 2Response
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{"error":null,"data":[{"name":"ada"},{"name":"bob"}],"root":{"count":2}}Output
// `data` is a JSON array of row objects.
// `root.count` is the number of rows in `data`.
// (See "Two builders, two root shapes" — the /api/qql fast path
// emits the engine envelope verbatim, whose root carries `count`.)Error envelope#
When a function returns any non-OK code, data is null and error is { "code", "message" }. The HTTP status mirrors the stable code (see the mapping table below). The code string is stable — clients should branch on it, not on the human message.
Request
POST /api/qql HTTP/1.1
Host: localhost
Content-Type: text/plain
SELECT * FROM _sys_usersResponse
HTTP/1.1 403 Forbidden
Content-Type: application/json; charset=utf-8
{"error":{"code":"forbidden","message":"access denied"},"data":null,"root":{"took_us":0,"request_id":""}}Output
// The HTTP status (403) mirrors the stable code ("forbidden").
// Router-level errors use error_envelope(), whose root is
// {"took_us":0,"request_id":...}.Router-level errors (unknown host, no matching route, batch body invalid, scope denial) are produced by error_envelope(code_str, status, message, request_id). Function-level errors are produced by envelope(...) and carry the real took_us and request_id.
Error code -> stable string -> HTTP status#
code_status in src/workers/envelope.rs is the single source of truth that maps every internal ResponseCode variant to a stable wire string and an HTTP status. Eleven internal variants collapse to eight distinct stable codes (for example both PublicFunctionNotFound and InternalFunctionNotFound map to not_found; both WorkersBusy and RuntimeClosed map to workers_busy).
| Internal ResponseCode | Stable code | HTTP status |
|---|---|---|
| Ok | ok | 200 |
| WorkersBusy | workers_busy | 503 |
| RuntimeClosed | workers_busy | 503 |
| PublicFunctionNotFound | not_found | 404 |
| InternalFunctionNotFound | not_found | 404 |
| AccessDenied | forbidden | 403 |
| InvalidInput | bad_request | 400 |
| ResourceLimit | rate_limited | 429 |
| FunctionFailed | function_failed | 422 |
| FunctionPanicked | internal | 500 |
| InternalError | internal | 500 |
| Distinct stable code | HTTP | Meaning |
|---|---|---|
| ok | 200 | Success. |
| workers_busy | 503 | Worker pool saturated or runtime closing; retry. |
| not_found | 404 | No such public/internal function (or no matching route/host). |
| forbidden | 403 | Access denied by scope/auth/RBAC. |
| bad_request | 400 | Invalid input (bad QQL, malformed batch body, etc.). |
| rate_limited | 429 | A resource limit was hit. |
| function_failed | 422 | The function ran but failed (e.g. a failed helper in a chain). |
| internal | 500 | A panic or internal error. |
OutputKind: how `data` is shaped#
A route declares an OutputKind (see src/workers/model.rs) that tells the worker how to turn the handler's raw bytes into the data value. There are five kinds.
- Passthrough
- The handler already produced the complete envelope JSON (e.g. the
qqlengine response). It is sent verbatim, not re-wrapped. On an error short-circuit, the standard error envelope is rendered instead. This is what/api/qqland/api/qql/batchuse. - RawJson
- The handler's bytes are an already-valid JSON fragment; they are embedded directly as the
datavalue inside the envelope. Used by/api/specand/api/spec/openapiso the cached catalog/spec appears unescaped underdata. - Record
- The handler's bytes are an encoded DMS
Record; the worker decodes it and emits it as a JSON object (aValue::Map). If decoding fails it falls back to a JSON string. - Text
- Raw UTF-8 text, embedded as a JSON string in
data(escaped). - Html
- UTF-8 HTML, sent as the response body with
Content-Type: text/html— it bypasses the JSON envelope entirely on success. On error it falls back to the JSON error envelope.
| OutputKind | data is... | Content-Type | Wrapped in envelope? |
|---|---|---|---|
| Passthrough | the handler's full envelope, verbatim | application/json | already an envelope |
| RawJson | embedded JSON fragment | application/json | yes |
| Record | decoded record -> JSON object | application/json | yes |
| Text | escaped JSON string | application/json | yes |
| Html | raw HTML body | text/html | no (success); yes (error) |
Two builders, two root shapes#
There are two envelope builders, and they put different fields in root. Knowing which one ran tells you which metadata to expect.
1) The worker envelope builder envelope(resp, kind, root) and the router-level error_envelope(...) set root to { "took_us": <int>, "request_id": <string> }. took_us is the pipeline duration in microseconds (measured around run_pipeline); request_id is a 16-hex-digit id assigned per request. Router-level errors set took_us: 0 and an empty or fixed request_id.
2) The QQL engine's own envelope (Response::to_json in src/functions/db/engine.rs) sets root to { "count": <int> }, where count is the number of rows in data. Because /api/qql and /api/qql/batch are Passthrough routes, this engine envelope is sent to the client verbatim — so a SELECT over /api/qql returns a root with count (and not took_us/request_id).
The OpenAPI projection models root as the union of all three fields (took_us, request_id, count, all optional integers), since which appear depends on the route and code path.
root fields reference#
- took_us
- Integer. Pipeline duration in microseconds. Present on envelopes built by the worker (
envelope/error_envelope). Router-level errors report 0. - request_id
- String. A 16-hex-digit id assigned per request (
format!("{:016x}", next_id())). Present on worker-built envelopes; empty on some router-level/fast-path responses. - count
- Integer. Number of rows in
data. Present on the QQL engine envelope (thedatavalue being a list of row maps, or 1 for a single record).
Helpers in the envelope module#
src/workers/envelope.rs also exposes utilities the worker uses around the envelope.
- value_to_json(&Value)
- Serializes a DMS
Valueto JSON: ints/decimals/timestamps exact; non-finite floats -> null; bytes as a hex string; vectors and lists as arrays; maps as objects. - json_escape(&str)
- Escapes a string for JSON (quotes, backslash, control chars as \u00xx).
- etag(&[u8])
- FNV-1a 64-bit hex over the response body, used for weak ETags on non-fast-path routes.