# Request Lifecycle

- Canonical URL: https://docs.fairvisor.com/docs/reference/request-lifecycle/
- Section: docs
- Last updated: n/a
> Detailed evaluation order and decision flow inside Fairvisor Edge.


This page is the technical deep-dive for how each request is processed.

## Request lifecycle

Every HTTP request that reaches the enforcement point goes through the following steps in order.

### 1. Context extraction

`build_request_context()` assembles a normalized view of the request:

- **Host** — request host used by selector `hosts` filtering
- **Headers** — stored with three key variants per header: as-is, underscore form, and hyphen form, so `X-API-Key`, `x_api_key`, and `x-api-key` all resolve correctly
- **JWT payload** — the `Authorization: Bearer <token>` header is base64url-decoded (payload only, no signature verification)
- **IP address** — taken from `ngx.var.remote_addr`
- **User-Agent** — read lazily only when active policy keys require `ua:*` descriptors
- **Query parameters** — parsed from `ngx.var.query_string`

### 2. Bundle check

If no bundle is loaded yet (edge is still starting), the request is rejected with `503 Service Unavailable` and reason `no_bundle_loaded`.

### 3. Runtime overrides and kill switch scan

Before the kill-switch scan, runtime override blocks are checked:

- `global_shadow` active -> all matched policies are treated as shadow mode
- `kill_switch_override` active -> kill-switch scan is skipped

If `kill_switch_override` is not active, kill switches are evaluated **before** any route matching.

The engine scans every entry in `bundle.kill_switches`:

1. If the entry has `expires_at` and that time is past, the entry is skipped.
2. The descriptor for the entry's `scope_key` is extracted from the request context.
3. If the descriptor value equals `scope_value` (exact match), and the optional `route` field also matches — the request is immediately rejected with `reason: kill_switch` and a `Retry-After: 3600` header.

**First match wins.**

### 4. Route matching

`route_index.match(host, method, path)` matches selectors in two stages:

- Host filter (`selector.hosts`) is evaluated first.
- `pathPrefix` selectors collect matches at every level of the trie.
- `pathExact` selectors match only at the leaf node.
- `methods` filters are applied per-policy after trie traversal.

If no policies match, the request is allowed with `reason: no_matching_policy`.

### 5. Per-policy evaluation

For each matched policy (in index order):

#### 5a. Shadow mode detection

If `policy.spec.mode == "shadow"` **or** top-level `global_shadow` is active, limiter state for this policy is namespaced under a `shadow:` prefix. A would-reject is recorded but traffic is never blocked.

#### 5b. Loop detection

If `loop_detection.enabled`, a fingerprint is computed from request inputs and descriptors. A counter is incremented in shared dict with TTL = `window_seconds`. If the counter exceeds `threshold_identical_requests`, the configured action (reject / throttle / warn) is applied.

#### 5c. Circuit breaker

If `circuit_breaker.enabled`, spend rate for the policy key is checked. If breaker is open, request is rejected with `reason: circuit_breaker_open`.

#### 5d. Rule matching

Rules are evaluated in definition order. Each rule may have a `match` block that filters by descriptor equality. If no rules match and `fallback_limit` exists, fallback is used.

#### 5e. Per-rule limiter

The algorithm in `rule.algorithm` is invoked:

| Algorithm | Module | State key prefix |
|---|---|---|
| `token_bucket` | `token_bucket.lua` | `tb:` |
| `cost_based` | `cost_budget.lua` | `cb:` |
| `token_bucket_llm` | `llm_limiter.lua` | `tpm:` / `tpd:` |

All limiters use `ngx.shared.dict` (`fairvisor_counters`) for state.

If any limiter denies the request, evaluation stops and reject is returned.

### 6. Allow or reject

- **Allow**: `200 OK` (decision service) or pass-through (reverse proxy). Rate-limit headers may be set.
- **Reject**: `429 Too Many Requests` with:
  - `X-Fairvisor-Reason`
  - `Retry-After` (deterministic per-identity jitter)
  - `RateLimit`, `RateLimit-Reset`
  - optionally `RateLimit-Limit`, `RateLimit-Remaining`
- **Throttle**: worker sleeps `delay_ms` (max 30s), then allows.

Policy/rule attribution is available via debug-session headers (`X-Fairvisor-Debug-*`), not standard reject headers.

## Fail-open semantics

| Condition | Behaviour |
|---|---|
| No bundle loaded | 503 (intentional; misconfiguration signal) |
| `ngx.shared.dict` write error | Allow (logged as warning) |
| Missing descriptor key | Rule is skipped; warning logged |
| JSON parse error in body | Token estimation falls back to `body_length / 4` |
| SaaS unreachable | Last known bundle remains active |

## Evaluation order summary

```text
Request in
  -> Runtime overrides check
       -> kill_switch_override active? skip kill-switch scan
  -> Kill switches (first match -> 429, when enabled)
  -> Route index match (host + method + path)
       -> no match -> allow
  -> For each matched policy:
       -> loop detection (optional)
       -> circuit breaker (optional)
       -> for each matching rule:
            -> limiter check
                 -> deny -> 429 (or shadow-allow)
                 -> allow -> continue
  -> all policies passed -> allow
```

