Skip to content

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 2

Response

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_users

Response

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 ResponseCodeStable codeHTTP status
Okok200
WorkersBusyworkers_busy503
RuntimeClosedworkers_busy503
PublicFunctionNotFoundnot_found404
InternalFunctionNotFoundnot_found404
AccessDeniedforbidden403
InvalidInputbad_request400
ResourceLimitrate_limited429
FunctionFailedfunction_failed422
FunctionPanickedinternal500
InternalErrorinternal500
Distinct stable codeHTTPMeaning
ok200Success.
workers_busy503Worker pool saturated or runtime closing; retry.
not_found404No such public/internal function (or no matching route/host).
forbidden403Access denied by scope/auth/RBAC.
bad_request400Invalid input (bad QQL, malformed batch body, etc.).
rate_limited429A resource limit was hit.
function_failed422The function ran but failed (e.g. a failed helper in a chain).
internal500A 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 qql engine response). It is sent verbatim, not re-wrapped. On an error short-circuit, the standard error envelope is rendered instead. This is what /api/qql and /api/qql/batch use.
RawJson
The handler's bytes are an already-valid JSON fragment; they are embedded directly as the data value inside the envelope. Used by /api/spec and /api/spec/openapi so the cached catalog/spec appears unescaped under data.
Record
The handler's bytes are an encoded DMS Record; the worker decodes it and emits it as a JSON object (a Value::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.
OutputKinddata is...Content-TypeWrapped in envelope?
Passthroughthe handler's full envelope, verbatimapplication/jsonalready an envelope
RawJsonembedded JSON fragmentapplication/jsonyes
Recorddecoded record -> JSON objectapplication/jsonyes
Textescaped JSON stringapplication/jsonyes
Htmlraw HTML bodytext/htmlno (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 (the data value 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 Value to 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.