Skip to content

Qirava DMS docs

Worker pipeline: before -> handle -> after

A worker turns one inbound protocol request into an ordered sequence of function calls: before -> handle -> after. The before phase authenticates and prepares shared state; the handler does the one piece of real work; the after phase observes the committed response (audit, session-extend) and can never change it. A per-request RequestContext is threaded through all three phases so request data is built once and reused. Groups bind these chains to sets of routes, and each chain runs in Chain (sequential, short-circuiting) or All (concurrent) mode. This page is the complete reference for that pipeline.

The three phases#

  RequestContext (worker, route, method, path, params, query, headers, body, required_role, shared)
        │
   ┌────┴────┐    ┌─────────┐    ┌─────────┐
   │ before  │ -> │ handler │ -> │  after  │
   │ (gates) │    │ (1 fn)  │    │(observe)│
   └─────────┘    └─────────┘    └─────────┘
   short-circuits  raw body /     ALWAYS runs,
   on non-ok       query record   read-only,
                   / record       errors ignored
before -> handle -> after, with ctx threaded through
before
Groups covering the route run in worker-declaration order. Within a group, Chain runs sequentially (ctx evolves as each before returns a record that is merged into shared state; a non-ok result short-circuits) and All runs concurrently over one ctx snapshot. A non-ok before skips the handler and becomes the response verbatim.
handler
Exactly one function. Its input depends on the route: a qql data route renders its query template from params/body and passes the final QQL; a raw_input route gets the request body bytes; everything else gets an encoded request Record.
after
Always runs, even on a before/handler error. It is read-only and gets an after_record that includes the handler's response bytes. Its errors are IGNORED so an after bug can never mask a committed response. This is where session-extend and audit live.

The shared context#

RequestContext is built once per request. before-functions write into its shared state by returning a Record, which run_before merges (merge_shared; later keys override earlier ones). The handler and after-functions read that accumulated state. This is why the request's data is built once and reused instead of re-querying the db within one request.

rust
// src/workers/context.rs — the per-request record passed to before/after/non-raw handlers
pub fn request_record(&self) -> Record {
    Record::new()
        .with("worker", ...).with("route", ...).with("method", ...).with("path", ...)
        .with("params", ...).with("query", ...).with("headers", ...).with("body", ...)
        .with("required_role", ...)   // from Route::require_role, for L1 to enforce
        .with("ctx", Value::Map(self.shared.clone())) // everything before-fns accumulated
}

Groups: bind chains to routes#

A Group binds before/after chains to a set of route ids. One group can cover many routes (define auth once, apply to many); a route can be in many groups, which run in worker-declaration (priority) order. A Bound is one function in a chain; Bound::wait(key) blocks the chain on its result, Bound::fire(key) is fire-and-forget (dispatched, never affects the response — for audit/notify).

rust
// src/workers/apps/system/routes.rs — the real Studio session group
pub fn studio_session_group() -> Group {
    Group::new("studio_session")
        .for_routes(STUDIO_ROUTE_IDS)
        .before(ChainMode::Chain, vec![Bound::wait(AUTH_SESSION)])        // validate cookie, inject {user,role}
        .after(ChainMode::Chain,  vec![Bound::wait(AUTH_SESSION_EXTEND)]) // bump idle TTL on success
}

Example: a request through the pipeline#

Take a gated Studio page. The studio_session group's before runs auth.session, which validates the qdms_session cookie and returns a record {user, role}; run_before merges that into shared state. The handler (the page) renders HTML using ctx.role. The after runs auth.session_extend, which checks the committed response and bumps the session idle window only on success. Here is the success case and the short-circuit case.

Request

GET /studio/users HTTP/1.1
Cookie: qdms_session=<valid token>

Response

HTTP/1.1 200 OK
Content-Type: text/html

<!doctype html> ... the Users & RBAC screen ...

Output

before: auth.session OK -> merges {user:"ada", role:"admin"} into ctx
handler: USERS_HANDLER renders HTML (sees ctx.role="admin")
after:   auth.session_extend sees a non-error body -> bumps expires_us

Request

GET /studio/users HTTP/1.1
(no Cookie)

Response

HTTP/1.1 403 Forbidden
Content-Type: application/json

{"error":{"code":"forbidden","message":"missing session token"},"data":null,"root":{...}}

Output

before: auth.session returns AccessDenied -> handler is SKIPPED;
the denial becomes the response. after still runs but extend=false (response_failed).

Chain vs All, and short-circuiting#

PhaseChain modeAll mode
beforesequential; ctx evolves between calls; first non-ok short-circuits the phase and skips the handlerconcurrent over ONE ctx snapshot; all waited; first non-ok short-circuits
aftersequential; runs over the committed response; errors ignoredconcurrent; runs over the committed response; errors ignored