Skip to content

Query pipeline

A query goes through the same guard stages as a command, then a second set of stages that shape the result. Knowing the order tells you where a renderer, an interceptor, or a guard sees the data, and why a request for sorting can fail after your method already ran.

StepYou configure it withWhen it fails
1. Authenticationauthentication handlers, or a native principal401
2. Tenanttenancy.httpHeader, tenancy.sources, or tenancy.resolve400 or 403 only when tenancy requires it
3. Read argumentsThe query string for GET, the body for QUERY400 malformedRequest for rejected input; an unreadable QUERY body produces a 400 exception envelope, while invalid paging produces a 400 rule result
4. Declared authorization@roles, @authorize, @allowAnonymous, or authorization403
5. Bind argumentsargument(...) descriptors, or a Zod schemaA wrong shape is held until global authorization finishes, then 400 malformedRequest
6. Per-request authorizationauthorize(input, context) on a successfully bound low-level definition403
7. Global authorization filtersScoped AuthorizationQueryFilter services403 for denial; a thrown filter fails closed with 500
8. Global ordinary filtersScoped QueryPipelineFilter services400 for validation, 500 for exceptions
9. ValidationQueryValidator, concept validators, validate, filters400 with every result
10. PerformYour query method, or perform / observe500 when it throws

The allowed severity for queries is always Warning. Query filters run once at admission, before validator and performer dependencies are constructed. A malformed schema shape reaches authorization filters with raw arguments but does not reach ordinary filters; transport argument coercion errors return before filters. These stages match the command pipeline.

After the method returns, Arc shapes the value in the same request or subscription scope:

  1. Renderers. The first registered renderer whose canRender accepts the value turns it into data or a queryPage.
  2. Read-model interceptors. Each registered interceptor for the exact runtime class of an item transforms it, including items inside a provider-owned page. An interceptor that implements isReleased also protects nested instances of its model: the query fails if one is found that it does not report released.
  3. Sorting and paging. An array is sorted, then paged, in memory. A queryPage is used as is. See Paging and sorting.
  4. Encoding. Decorated models and concepts become their wire shape.

For an observable query, the guard stages (including global query filters) run once when the subscription opens. Result stages 1 to 4 run for the current snapshot and for every emission, and emission guards run after rendering, before each delivery. A pre-aborted query starts no declared authorization or named policy. Arc checks cancellation before each declared authorization policy resolution and invocation and after each awaited resolution; the same admission rule applies to filters, validators, dependency resolution, perform(), renderer and interceptor service resolutions and invocations, and render(). If cancellation occurs during a policy or result-stage callback, Arc waits for that callback to settle, then fails without starting the next policy or result stage or delivering successful data. In-flight effects are not undone; request and subscription scopes still close.

  • A request for paging or sorting on a non-array, non-page result answers 400 after the method has run.
  • An interceptor never sees a value a renderer turned into something other than its exact class.
  • Interceptors and emission guards do not replace admission authorization; decide initial access in the guard stages.