# HTTP Server CLI — Middlewares

Middlewares in the HTTP Server CLI follow an **onion pipeline** pattern. Each middleware wraps the next, allowing logic to run before (pre-processing) and after (post-processing) the request handler.

## Registration Scopes

Middlewares can be registered at three levels:

### Global (SAPI)

Applied to **every** request processed by the server:

```php
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Request;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Response;
use Bootgly\API\Workables\Server as SAPI;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\CORS;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Compression;

SAPI::$Middlewares->prepend(new CORS);       // Add to the beginning
SAPI::$Middlewares->append(new Compression); // Add to the end
SAPI::$Middlewares->pipe(new CORS, new Compression); // Add multiple at once
```

### Route Group

Applied to all routes defined after `intercept()`, scoped to the current Router context:

```php
$Router->intercept(
   new CORS,
   new RateLimit(limit: 100, window: 60, scope: 'public-api')
);

yield $Router->route('/api/:*', function ($Request, $Response) use ($Router) {
   // All routes inside this group inherit CORS + RateLimit
   yield $Router->route('/users', function ($Request, $Response) {
      return $Response->JSON->send(['users' => []]);
   }, GET);
}, GET);
```

### Route Level

Applied to a single specific route:

```php
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\RateLimit;

yield $Router->route('/login', function ($Request, $Response) {
   // ...
}, POST, middlewares: [new RateLimit(limit: 5, window: 60, scope: 'auth-login')]);
```

When both group and route-level middlewares are present, they are **merged** — group middlewares execute first, then route-level ones, forming a single onion pipeline around the handler.

## Registration Methods

| Method | Description |
|---|---|
| `prepend(Middleware $Middleware)` | Add a middleware to the **beginning** of the pipeline |
| `append(Middleware $Middleware)` | Add a middleware to the **end** of the pipeline |
| `pipe(Middleware ...$middlewares)` | Add one or more middlewares to the end at once |

## Built-in Middlewares

All built-in middlewares are in the namespace `Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares`.

---

### Authentication

Protects routes with ordered Basic, Bearer, JWT, and Session guards. Authentication is configured with an `Authenticating` guard strategy and executed by the `Authentication` middleware.

```php
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Authenticating;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Authentication;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Authentication\Bearer;

$Bearer = new Authenticating(
   new Bearer(function (string $token): bool {
      return $token === 'demo-bearer-token';
   })
);

yield $Router->route('/private', $Handler, GET, middlewares: [new Authentication($Bearer)]);
```

See the [Authentication](/manual/WPI/HTTP/HTTP_Server_CLI/Authentication/) page for Bearer, JWT, Basic, Session, middleware-owned challenges, and demo routes.

**Phase:** Pre-processing — rejects unauthenticated requests before the handler runs.

---

### Authorization

Protects authenticated routes with ordered Scope, Role and Policy gates. Authorization is configured with an `Authorizing` gate strategy and executed by the `Authorization` middleware.

```php
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Authorizing;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Authorization;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Authorization\Scope;

$Authorizing = new Authorizing(new Scope('demo:read'));

yield $Router->route('/private', $Handler, GET, middlewares: [new Authorization($Authorizing)]);
```

See the [Authorization](/manual/WPI/HTTP/HTTP_Server_CLI/Authorization/) page for Scope, Role, Policy gates, denial responses and the API/RBAC boundary.

**Phase:** Pre-processing — rejects unauthorized requests before the handler runs.

---

### CORS

Handles Cross-Origin Resource Sharing validation and preflight (`OPTIONS`) requests.

```php
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\CORS;

new CORS(
   origins: ['https://example.com'],      // Allowed origins (default: [] — empty allowlist; pass ['*'] for wildcard)
   methods: ['GET', 'POST'],              // Allowed methods (default: ['GET','POST','PUT','DELETE','PATCH','OPTIONS'])
   headers: ['Content-Type'],             // Allowed headers (default: ['Content-Type','Authorization'])
   maxAge: 86400,                         // Preflight cache in seconds (default: 86400)
   credentials: false                     // Allow credentials (default: false)
);
```

> **Secure by default.** `origins` defaults to an **empty allowlist** — every cross-origin
> request is rejected (`403`) until you list the origins you trust. Pass `origins: ['*']` to opt
> into a wildcard (origin-independent) policy. When a request's `Origin` is reflected (allowlist
> match), `Vary: Origin` is emitted so a shared cache (CDN / reverse proxy) never serves one
> origin's response to another. An allowlist with no request `Origin` emits no
> `Access-Control-Allow-Origin` — it never falls back to `*`.

**Phase:** Pre-processing — validates the origin and handles preflight before the handler runs.

---

### Compression

Compresses response bodies using `gzip` or `deflate` based on the client's `Accept-Encoding` header.

```php
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Compression;

new Compression(
   level: 6,        // Compression level 1-9 (default: 6)
   minSize: 1024    // Minimum body size in bytes to compress (default: 1024)
);
```

**Phase:** Post-processing — compresses the response body after the handler produces it.

> **Only 2xx/3xx.** Only success and redirect bodies are compressed — `4xx`/`5xx` error and auth-challenge responses are left untouched.

---

### ETag

Generates and validates ETags for HTTP caching. Returns `304 Not Modified` when the client's `If-None-Match` header matches.

```php
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\ETag;

new ETag(
   weak: true  // Use weak ETags (default: true)
);
```

**Phase:** Post-processing — computes the ETag from the response body after the handler runs.

> **Only 2xx/3xx + RFC 7232.** An ETag is set (and `304` revalidation performed) only for success/redirect responses — never for `4xx`/`5xx` error or auth-challenge bodies. `If-None-Match` is evaluated per RFC 7232: `*` matches any representation, comma-separated lists are supported, and the weak comparison ignores the `W/` prefix. Order `ETag` **outside** `Compression` so the tag covers the encoded (delivered) body.

---

### RateLimit

Enforces rate limiting by tracking request counts per principal within time windows. The immutable TCP peer is the principal by default. Returns `429 Too Many Requests` when exceeded.

```php
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\RateLimit;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\RateLimit\Algorithms;

new RateLimit(
   limit: 60,                        // Maximum requests per window (default: 60)
   window: 60,                       // Time window in seconds (default: 60)
   trustForwarded: false,            // Key on the proxy-resolved $Request->address (default: false)
   ipv6Prefix: 64,                   // Aggregate IPv6 keys to this prefix (default: /64)
   globalLimit: 0,                   // Optional cross-worker aggregate ceiling (default: 0 = off)
   algorithm: Algorithms::Sliding,   // Counting algorithm (default: Sliding; or Fixed)
   key: null,                        // Custom principal resolver fn (Request): ?string (default: IP)
   scope: 'public-api',              // Stable per-principal policy identity (default: derived source line)
   globalScope: null                 // Aggregate policy identity (default: scope)
);
```

**Counter key (security).** By default the limiter keys on `$Request->peer` — the **immutable TCP transport IP**, which a client cannot forge. This is deliberate: `TrustedProxy` can overwrite `$Request->address` from a client-supplied `X-Forwarded-For` header, so keying on `$address` would let a client behind (or co-located with) a trusted proxy rotate that header and open a fresh rate-limit bucket per request, evading the limit entirely.

Set `trustForwarded: true` **only** when the server sits behind a genuinely trusted proxy and you want per-real-client buckets — it makes the limiter key on `$Request->address` (the proxy-resolved client IP). Combine it with a correctly configured `TrustedProxy` so that address is itself trustworthy.

**IPv6 aggregation.** A single client is routinely allocated a whole `/64`, giving 2⁶⁴ distinct `/128` addresses. Keying on the full address would let such a client mint a fresh bucket per request, so IPv6 keys are masked to `ipv6Prefix` (default `/64`) — every address in the same `/64` shares one counter. IPv4 keys are used in full. Lower the prefix (e.g. `/56`, `/48`) to aggregate even more aggressively.

**Algorithm.** `Algorithms::Sliding` (default) is a weighted sliding window: it blends the current and previous windows by how much of the previous window is still in view, so a client cannot send `2 × limit` by bursting across a window boundary. `Algorithms::Fixed` is the cheaper classic counter (one key, resets on TTL) if you do not need boundary smoothing.

**Policy scope.** `scope` identifies the logical policy; `key` identifies the principal inside that policy. When `scope` is omitted, Bootgly derives an automatic identity from the normalized source file and line containing the `new RateLimit` expression. The same construction site therefore shares counters when lazy route declarations are built independently by different workers, while different source lines are isolated. Use an explicit stable scope for factories, subclasses, generated definitions, multiple logical policies created on one physical line, and security-critical rolling deployments: reusing a construction site unintentionally shares a policy, while moving it changes the automatic identity and starts a fresh quota. Explicit `scope` and `globalScope` values must contain between 1 and 256 bytes and cannot be empty or whitespace-only. A shared principal scope still cannot mix incompatible algorithms or windows; those semantics remain partitioned.

**Global ceiling.** `globalLimit` (default `0` = off) adds one cross-worker aggregate counter per `globalScope` and window on top of the per-principal limit. When `globalScope` is omitted, it inherits `scope`, so independent policies have independent aggregate ceilings by default. Requests are counted globally only after they pass the per-principal check.

Route-disjoint policies can deliberately share one aggregate ceiling by using distinct principal scopes and the same explicit `globalScope`, cache namespace, window, and `globalLimit`:

```php
$LoginLimit = new RateLimit(
   limit: 5,
   window: 60,
   globalLimit: 1_000,
   scope: 'auth-login',
   globalScope: 'public-auth'
);

$ResetLimit = new RateLimit(
   limit: 3,
   window: 60,
   globalLimit: 1_000,
   scope: 'auth-reset',
   globalScope: 'public-auth'
);
```

Do not stack middleware instances with the same `globalScope` in one request pipeline: every middleware invocation increments the shared aggregate counter, so one request would be counted more than once.

**Custom key.** `key` is a resolver `fn (object $Request): ?string`. Return a string to rate-limit on something other than the IP — an API key owner, an authenticated user id, a tenant — or `null` to fall back to the default IP key. Authenticate and map untrusted credentials to a bounded, stable principal ID; never return a raw header value. Here `$APIKeys` represents an application authentication service whose `resolve()` returns that ID or `null`.

```php
// Rate-limit by the validated API-key owner instead of IP:
new RateLimit(
   limit: 1000,
   window: 3600,
   scope: 'api-key-hourly',
   key: static fn (object $Request): ?string =>
      $APIKeys->resolve($Request->Header->get('X-Api-Key'))
);
```

> [!NOTE]
> **Namespace migration.** Upgrading from legacy counters without policy scopes starts fresh counters once. During a mixed-version rolling deployment, old and new workers do not share one quota; drain the old workers or otherwise account for that temporary split. After the migration, use explicit scopes wherever quota continuity must survive source movement or rolling releases.

> [!WARNING]
> **Shared-memory capacity.** The default Shared backend has fixed capacity. Expired records stop counting, but their segment space is not automatically reclaimed until an explicit `$Cache->purge()`; `clear()` or segment removal also reclaims space but resets every live entry sharing that segment. Sliding windows and changing principals create new records over time, and a custom `key` resolver can accelerate growth. Policy-scope hashing does not bound the custom principal key stored beside it. Never return unbounded, unauthenticated attacker-controlled values directly: bound both input length and cardinality and prefer stable authenticated identifiers. Hashing the principal yourself limits each entry's key size, but not the number of entries. For long-lived Shared deployments, manage an injected cache and arrange periodic purge outside the request hot path, or choose a backend whose reclamation and operational trade-offs fit the workload. Capacity exhaustion can surface as HTTP `500` responses. See [Cache](/guide/cache/overview/) for backend behavior.

**Phase:** Pre-processing — rejects requests that exceed the rate limit before reaching the handler.

---

### BodyParser

Validates and enforces maximum request body size. Returns `413 Content Too Large` when exceeded.

```php
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\BodyParser;

new BodyParser(
   maxSize: 1_048_576  // Maximum body size in bytes (default: 1 MB)
);
```

**Phase:** Pre-processing — validates the body size before the handler processes it.

---

### CSRF

Synchronizer-token CSRF protection. Generates a per-session token, stores it on `$Request->Session`, and validates submitted tokens on unsafe HTTP methods (`POST`, `PUT`, `PATCH`, `DELETE`) using a timing-safe comparison.

```php
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\CSRF;

new CSRF(
   sessionKey: '_csrf_token',     // Session key for token storage (default: '_csrf_token')
   headerName: 'X-CSRF-Token',    // Request header carrying the token (default: 'X-CSRF-Token')
   formField: '_token',           // Form field carrying the token (default: '_token')
   checkOrigin: false,            // Validate Origin/Referer hostname against Host (default: false)
   allowedOrigins: [],            // Trusted cross-origin hostnames when checkOrigin=true (default: [])
   tokenBytes: 32                 // Random bytes; token is hex-encoded (default: 32 → 64-char token)
);
```

The token is read from the `X-CSRF-Token` header **or** the `_token` form field. Safe methods (`GET`, `HEAD`, `OPTIONS`) emit the token but skip validation. Unsafe methods that fail validation are rejected with `403 Forbidden`:

- `Invalid CSRF token` — token missing or mismatched.
- `Invalid CSRF origin` — only when `checkOrigin: true` and the `Origin` (fallback `Referer`) hostname does not match `Host` nor any entry in `allowedOrigins`.

The token rotates only when you call `$Request->Session->regenerate()` (e.g. after login or privilege escalation). Comparison uses `hash_equals()` to prevent timing attacks.

**Render the token masked (BREACH protection).** The session token is a stable secret; rendering it raw into a body that is also compressed (see [Compression](#compression)) and reflects attacker input exposes it to a BREACH compression-length oracle. Render `CSRF::mask()` instead of the raw token — it returns a per-response value (`hex(nonce ‖ (token XOR nonce))`, different every call), so there is no stable secret in the body. Validation unmasks the submitted token automatically (and still accepts a raw token, so existing forms keep working).

```php
// In your view/template — emit a masked token, never the raw session value:
<input type="hidden" name="_token"
       value="<?= CSRF::mask($Request->Session->get('_csrf_token')) ?>">
```

```php
// Equivalent for an API/JS client (e.g. a meta tag the front end reads):
$masked = CSRF::mask($Request->Session->get('_csrf_token'));
```

> An app that renders *its own* secret (an API key, a session value) alongside reflected request input in a compressed response should still avoid compressing that response — masking only covers the framework's CSRF token.

**Phase:** Pre-processing — generates and validates the token before the handler runs.

---

### Validator

Fail-closed request validation. Runs a set of rules against one Request source (`Fields`, `Queries`, `Headers`, `Cookies`, or `Files`) and short-circuits with a JSON error response if any rule fails — the route handler never runs.

```php
use Bootgly\ADI\Validators\Email;
use Bootgly\ADI\Validators\Required;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Validator;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Validator\Sources;

new Validator(
   rules: [
      'email' => [new Required, new Email],
   ],
   Source: Sources::Fields,   // Fields | Queries | Headers | Cookies | Files
   code: 422,                  // HTTP status on validation failure (default: 422)
   fallback: null              // Optional Closure(Request, Response, Validation): object
);
```

The default failure response is `422 Unprocessable Entity` with body `{"errors": {"email": ["email must be a valid email address."]}}`. Provide a `fallback` closure to render a custom error response while keeping the route fail-closed.

With `Sources::Headers`, rule keys match header names case-insensitively (RFC 9110) — a rule keyed `'X-API-Key'` binds to the header the client sent regardless of casing, and validation errors keep the key exactly as you wrote it. All other sources match keys case-sensitively.

See the [Request Validation](/manual/WPI/HTTP/HTTP_Server_CLI/Request/#request-validation) section for end-to-end examples, and the [ADI Validation](/manual/ADI/Validation/overview/) page for the full rule catalog and custom rules.

**Phase:** Pre-processing — validates input before the handler runs.

---

### RequestId

Generates or propagates unique request identifiers for distributed tracing and logging.

```php
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\RequestId;

new RequestId(
   header: 'X-Request-Id'  // Header name to read/write (default: 'X-Request-Id')
);
```

If the request already contains the specified header, the existing value is preserved. Otherwise, a new unique ID is generated.

**Phase:** Pre-processing — sets the request ID before the handler runs.

---

### SecureHeaders

Adds security headers to protect against common web vulnerabilities (XSS, clickjacking, MIME sniffing, etc.).

```php
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\SecureHeaders;

new SecureHeaders(
   contentSecurityPolicy: "default-src 'self'",  // CSP directive (default: "default-src 'self'")
   hsts: true,                                    // Enable HSTS (default: true)
   hstsMaxAge: 31536000                           // HSTS max-age in seconds (default: 31536000)
);
```

**Phase:** Post-processing — appends security headers to the response.

---

### TrustedProxy

Resolves the real client IP from trusted proxy headers (`X-Forwarded-For`, `X-Real-IP`) when the server runs behind a reverse proxy or load balancer.

```php
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\TrustedProxy;

new TrustedProxy(
   proxies: ['10.0.0.1']  // Trusted proxy IPs — set these explicitly in production
);
```

When the request comes from a trusted proxy IP, the middleware:
- Reads `X-Forwarded-For` (right-to-left, first untrusted hop) or `X-Real-IP` to update `$Request->address`
- Reads `X-Forwarded-Proto` to update `$Request->scheme`

Untrusted proxy IPs are ignored — the address and scheme are left unchanged.

**`$Request->address` vs `$Request->peer`.** This middleware only ever changes `$Request->address` (the application-facing client IP). The real socket peer is always available, unaltered, as **`$Request->peer`** — use it for anti-abuse decisions that must not be spoofable (rate limiting keys on it by default; see [RateLimit](#ratelimit)).

> **Security — set `proxies` explicitly in production.** When you construct `TrustedProxy` without a `proxies` argument it falls back to the localhost default (`127.0.0.1`, `::1`) and logs a one-time `WARNING` the first time it trusts a forwarded header. With that default, anything that can reach the server from localhost — a sidecar, an SSRF pivot, a dev port-forward — is trusted and can spoof `$Request->address` via `X-Forwarded-For`. Always pass the actual IPs of your reverse proxy / load balancer.

**Phase:** Pre-processing — resolves the real client IP before the handler runs. Only processes forwarded headers when the request originates from a trusted proxy IP.
