# Request Lifecycle

What actually happens between the TCP packet hitting the socket and
your handler returning a `Response`? Six files. Trace them once and
the framework's shape clicks into place.

## The path

```mermaid
flowchart TD
    bind["bind socket - server.rs"]
    accept["hyper accepts - HTTP/1.1 · h2 · WebSocket"]
    handle["handle_request<br/>WS upgrade? · health endpoint? · task-locals"]
    inner["handle_request_inner<br/>match_route · build the chain"]
    chain["execute_chain_safely<br/>panic boundary · middleware · handler"]
    resp(["HttpResponse on the wire"])

    bind --> accept --> handle --> inner --> chain --> resp
```

## 1. Boot - `app.rs`

A scaffolded app's `main()` builds an `Application` fluently and runs
it:

```rust
Application::new()
    .config(my_app::config::register)
    .bootstrap(my_app::bootstrap::bootstrap)
    .routes(my_app::routes::register)
    .migrations::<my_app::migrations::Migrator>()
    .run()
    .await
```

`Application::run()` parses the binary's CLI (clap):

- `serve` - start the HTTP server
- `web:run` - alias for serve
- `migrate` / `migrate:rollback` / `migrate:status` / `migrate:fresh`
- `schedule:run` / `schedule:work` / `schedule:list`
- `workflow:work`
- `queue:work`
- `down` / `up` - toggle maintenance mode

`db:sync` and `db:seed` live on the framework-wide `suprnova` CLI
binary (`suprnova-cli`) and the per-app `cmd/console` binary
respectively - not on the `Application::run()` switch.

`.env` is already loaded by this point. `#[suprnova::main]` loads it
*before* building the Tokio runtime, because writing to the process
environment is only sound while the process is single-threaded - see
[Bootstrap](bootstrap.md#suprnovamain-not-tokiomain). `Application::run`
refuses to boot if that step was skipped.

For `serve`, it then:

1. Verifies the environment was loaded from a single-threaded context
2. Drains the `#[policy]` inventory into the authorization system
3. Calls your `config_fn` (typed config registration)
4. Runs migrations
5. Calls your `bootstrap_fn` (service registration, observers, listeners)
6. Builds the `Router` from `routes_fn`
7. Hands the router to `Server::from_config(...)`
8. Calls `server.run()`

The same boot path is used by workers (`queue:work`, `workflow:work`,
`schedule:run`) so they see the same configured services and bound
container values.

## 2. Server boot - `server.rs`

`Server::from_config` does two things that matter for safety:

- Runs `App::init()` + `App::boot_services()` - initialises the
  container's task-local layer and resolves boot-time dependencies
- **Fails closed** when `APP_KEY` is required (any non-development
  environment) but missing/malformed - returns `Err`, and `app.rs`
  prints a remediation message and exits non-zero instead of panicking

`server.run()` then:

1. Boots telemetry (`tracing` subscriber, log format)
2. Loads encryption keys (`APP_KEY` + `APP_KEY_PREVIOUS`)
3. Boots the runtime drivers **in this exact order**: Cache → Queue →
   RateLimit → Mail. Non-server subcommands also call
   `bootstrap_runtime_drivers` so workers see the same drivers
4. Binds the TCP socket
5. Serves over hyper with `.with_upgrades()` (so WebSocket upgrades work)

The driver boot order is intentional - Queue may depend on Cache
(for unique-job locks), RateLimit may use Cache, Mail may dispatch
via Queue.

## 3. Request entry - `handle_request`

Every request lands in `handle_request(router, registry, req)`.
**This is also the in-process request surface integration tests
drive without opening a socket.** It's re-exported as
`suprnova::handle_request`.

```rust
pub async fn handle_request(
    router: Arc<Router>,
    middleware_registry: Arc<MiddlewareRegistry>,
    req: hyper::Request<hyper::body::Incoming>,
) -> hyper::Response<ServerBody>;
```

A peer-aware variant, `handle_request_with_peer`, takes the same
arguments plus an `Option<std::net::IpAddr>` - the production accept
loop uses it; in-process callers use `handle_request` and the request's
proxy headers (or `None`) determine `Request::ip()`.

Inside, it:

1. Checks for a WebSocket upgrade via `router.match_ws(...)` - if it
   matches a `ws!()` route, hands off to the WS handler
2. Special-cases the built-in health endpoints - `GET /_suprnova/health`,
   `/_suprnova/health/live`, `/_suprnova/health/ready`. A readiness probe
   that fails the `SERVER_HEALTH_READINESS_TOKEN` check is deliberately
   *not* special-cased: it falls through to routing and 404s like any
   unrouted path, so the endpoint is invisible rather than merely closed
3. Installs per-request task-locals (flash bag, SSR-disable flag)
4. Dispatches into `handle_request_inner`

## 4. Routing + chain assembly - `handle_request_inner`

This is where the middleware chain composes. The router yields a
`(pattern, handler, params)` triple, and the `MiddlewareChain` is
assembled in this fixed order:

```
[0] RequestIdMiddleware (always outermost)
[1] global middleware in registration order
[2] route middleware (keyed by (method, matched pattern))
[3] handler
```

Three things to notice:

- **Pattern, not path.** Route middleware is keyed by the matched
  pattern (`"/posts/{id}"`), not the raw path (`/posts/42`). Group
  middleware on parameterised routes actually fires.
- **No match still runs the chain.** If the router doesn't match any
  route, the chain (RequestId + globals) still runs and terminates in
  a registered fallback or a static 404. CORS preflight (OPTIONS rarely
  matches a route), logging, and request-id all reach unrouted traffic.
- **Group middleware is flattened, not stacked.** Group middleware is
  copied into each grouped route's middleware list at register time -
  it isn't a separate runtime layer. Introspection can't tell group
  from route middleware apart.

## 5. Panic boundary - `execute_chain_safely`

The chain runs inside `AssertUnwindSafe(...).catch_unwind()`. **A panic
in any middleware or the handler is caught**, logged with method+path,
and converted through the same `FrameworkError → HttpResponse` path
as a returned 5xx:

- Sanitised body: `{"message": "Internal Server Error"}`
- `request_id` injected so you can correlate with the log
- `ErrorOccurred` event dispatched so listeners (Sentry, your alert
  pipeline) see the failure
- The panic payload **never leaks into the response body**

This is a safety net, not a contract. Public APIs in your code should
return `Result`, not rely on `catch_unwind`. The boundary exists to
keep a buggy handler from killing the worker thread or leaking a stack
trace to the client - it isn't licence to `.unwrap()` everywhere.

## 6. Chain composition - `middleware/chain.rs`

`MiddlewareChain::execute` nests the handler as the innermost `Next`,
then wraps each middleware last-to-first (`.rev()`), so **the
first-added middleware runs first** (outside-in). An empty chain calls
the handler directly:

```
register order:   [Auth, CSRF, Throttle, handler]
runtime order:    Auth → CSRF → Throttle → handler → (back out)
```

If middleware short-circuits (returns `Err(response)`), the chain
unwinds immediately and the response goes back out through the
already-executed middleware in reverse.

## The `Response` contract

`http::Response` is **`Result<HttpResponse, HttpResponse>`** - both
arms carry an `HttpResponse`. Handlers and `Middleware::handle` return
`Response`:

- `Ok(resp)` is success
- `Err(resp)` short-circuits - for example, a 401 straight from auth
  middleware. The runtime collapses both with
  `result.unwrap_or_else(|e| e)`, so an `Err` is a response, not a
  crash.
- `?` propagates any error that converts to `HttpResponse`. Every
  `FrameworkError`, `AppError`, `ValidationErrors`, and your own
  `HttpError` impls do - so handler bodies read top-to-bottom and
  bubble failures to the converter.

The error converter (`From<FrameworkError> for HttpResponse`)
sanitises 5xx bodies and never leaks detail to the wire. The detail
stays in the structured log.

See [Error Handling](errors.md) and [Error Model](error-model.md) for
the full picture.

## Per-request state

Two layers of per-request state, both task-local:

- **Flash bag** - `req.flash()` returns the session flash; values stored
  here survive one redirect and then disappear
- **SSR-disable flag** - Inertia uses this to short-circuit
  server-side rendering in test contexts

Both are installed by `handle_request` before the chain runs and
torn down when the response leaves. Custom per-request state goes
through the `Context` system - see [Context](context.md).

## Workers reuse the same lifecycle

Background workers (`queue:work`, `workflow:work`, `schedule:run`) go
through:

1. The same boot path (`Config::init`, `bootstrap_runtime_drivers`,
   your `bootstrap()` function)
2. Their own loop that pulls work and runs handlers with the **same
   panic boundary** (`execute_chain_safely` equivalent for each worker
   type)
3. Graceful shutdown on `SIGTERM` / `SIGINT` - in-flight work finishes,
   no new work starts

This means an observer registered in `bootstrap()` fires for inserts
from a queue worker exactly as it would for inserts from an HTTP
handler.

## Production safety guarantees

A short list of invariants the lifecycle establishes:

- **`APP_KEY` is required in non-development environments.** Boot fails
  closed, exits non-zero, no encrypted-data corruption.
- **Panics in handler or middleware never reach the client.** The
  panic-boundary returns a sanitised 500 and dispatches `ErrorOccurred`.
- **5xx bodies are always sanitised.** Detail goes to the log, not the
  wire.
- **Poisoned locks never abort the process.** Two sanctioned patterns:
  per-request paths route poison into a `FrameworkError::Internal`
  carrying a `"<context> lock poisoned"` message (and the request
  gets a 500); hot-path registries that must stay up recover in place
  with `.unwrap_or_else(|e| e.into_inner())`. See
  [Lock Policy](lock-policy.md).
- **Driver backend failures are an explicit fail-open or fail-closed
  choice.** Rate-limit, cache, session each pick a policy at the call
  site - `BackendErrorPolicy::FailClosed` returns 503; `FailOpen`
  lets the request through. There is no implicit default. See
  [Rate Limiting](rate-limiting.md).
- **WebSocket upgrades go through the same router.** The same
  `match_ws` lookup uses the same `(method, pattern)` indexing as
  HTTP routes; you can apply per-route WS middleware exactly like
  HTTP middleware.
- **The shutdown signal is never starved by the connection cap.**
  With `SERVER_MAX_CONNECTIONS` set, waiting for a free slot races the
  shutdown signal rather than blocking the accept loop, so a server
  whose slots are all held by long-lived WebSocket sessions still
  drains on `SIGTERM` instead of being SIGKILLed at the end of the
  orchestrator's grace period.
- **Every drain aborts what it abandons.** HTTP connections, WebSocket
  handlers, and supervisors each get a bounded grace window and are
  then aborted and awaited - including a supervisor's inner task, so
  cancellation reaches the body and not just the restart wrapper.
  Nothing keeps running past its drain to emit telemetry after the
  flush.

## What this means for your code

A few takeaways for day-to-day handler writing:

- **Return `Response`, propagate with `?`.** Don't `match err` unless
  you need the bare `HttpResponse`.
- **Implement `HttpError` on your domain error types.** They'll
  convert automatically. See [Error Handling](errors.md).
- **Don't rely on the panic boundary.** It catches genuine bugs and
  prevents process crashes; library code should still return `Result`.
- **Middleware order matters and is fixed in three layers** -
  request-id outermost, globals next, route middleware innermost
  before the handler.
- **Workers and handlers share bootstrap.** Anything you register at
  boot is visible to both.

## Where each step lives

| Step | File |
|---|---|
| Boot | `framework/src/app.rs` |
| Server lifecycle | `framework/src/server.rs` |
| `handle_request` (entry) | `framework/src/server.rs` (re-exported as `suprnova::handle_request`) |
| `handle_request_inner` (routing + chain) | `framework/src/server.rs` |
| `execute_chain_safely` (panic boundary) | `framework/src/server.rs` |
| `MiddlewareChain::execute` (composition) | `framework/src/middleware/chain.rs` |
| Router matching | `framework/src/routing/router.rs` |

You shouldn't need to read these to use the framework, but if a bug
surprises you, the trail is short.

## Next

- [Service Container](container.md) - how `App::*` resolves services
- [Application Bootstrap](bootstrap.md) - what `bootstrap.rs` does
- [Middleware](middleware.md) - writing your own middleware
- [Error Model](error-model.md) - `FrameworkError`, `HttpError`,
  panic recovery in detail
- [Routing](routing.md) - what `routes!` actually expands into
