# HTTP Server CLI — Request

The `Request` object is automatically available in every route handler of the HTTP Server CLI. It provides a concise structure to access common request parameters such as headers, URI, query strings, and body content.

```php
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Request;
```

## Connection

`address`: The IP address from which the request originated.

```php
$Request->address; // '127.0.0.1'
```

`port`: The port number through which the request was transmitted.

```php
$Request->port; // '52252'
```

`scheme`: The protocol scheme, either `http` or `https`.

```php
$Request->scheme; // 'https'
```

## HTTP

`method`: The HTTP method used for the request.

```php
$Request->method; // 'GET'
```

`URI`: The Uniform Resource Identifier of the request. In the context of an HTTP Server, a URI will always have a scheme `http` or `https`, the domain and port will always be the same for the same Virtual Host (VHost). Therefore, a URI (identifier) in Bootgly is always everything that comes after the `domain:port` without the anchor/fragment (#):

```php
$Request->URI; // '/test/foo?query=abc&query2=xyz'
```

`protocol`: The protocol version used in the request, usually HTTP/1.1.

```php
$Request->protocol; // 'HTTP/1.1'
```

### Resource

`URL`: The URL (Uniform Resource Locator) path part of the URI. Still based on the above context of a URI on HTTP Servers in Bootgly and as a URL is a subset of a URI, in Bootgly a URL (locator) is the path where the resource is located, without the query string:

```php
$Request->URL; // '/test/foo'
```

`URN`: The URN (Uniform Resource Name) last path part of the URL. Based on the above context of a URL, a URN (name) is the last part (node) of a URL path and identifies the name of the resource. Obviously, this semantics only remains if its use in practice follows this same pattern.

```php
$Request->URN; // 'foo'
```

### Query

`query`: The query string portion of the URI.

```php
$Request->query; // 'query=abc&query2=xyz'
```

`queries`: An associative array of the parsed query string.

```php
$Request->queries; // Array ( [query] => abc [query2] => xyz )
```

## HTTP Header

`Header`: The Header class.

```php
$Request->Header->get('X-Requested-With'); // Get value of X-Requested-With Header
```

`headers`: The HTTP Headers in array.

```php
$Request->headers;
/*
Array (
  User-Agent] => BootglyHTTPClient/1.0
  [Accept] => */*,
  [Set-Cookie] => Array (
    [0] => user_id=123; Expires=Wed, 21 Oct 2024 07:28:00 GMT
    [1] => session_token=abc; HttpOnly
  )
)
*/
```

### Host Information

`host`: The fully qualified domain name of the server.

```php
$Request->host; // 'v1.docs.bootgly.com'
```

`domain`: The domain part of the host.

```php
$Request->domain; // 'bootgly.com'
```

`subdomain`: The subdomain part of the host.

```php
$Request->subdomain; // 'v1.docs'
```

`subdomains`: An array containing individual subdomain components.

```php
$Request->subdomains; // Array ( [0] => 'docs' [1] => 'v1' )
```

### Cookies

`Cookies`: The class that represents the HTTP Header Cookies of the request.

```php
$Request->Header->Cookies;
```

`cookies`: An array of cookies sent with the request.

```php
$Request->cookies; // Array ( [cookie_name] => cookie_value )
```

## HTTP Body

`Body`: The class that represents the HTTP Body of the request.

```php
$Request->Body;
```

### Input

`input`: The raw input content data from the request.

```php
$Request->input; // Raw input content data as string
```

A `multipart/form-data` body is the one exception: it never lands in `input`, which stays
empty for it. That body streams straight into `fields` and `files` while it is being
received, so reading `input` on an upload is expected to give you `''` — read `fields` and
`files` instead.

### Fields

`fields`: The decoded request body, as an associative array.

Parsing is dispatched on the `Content-Type`, never on the request method — `POST`, `PUT`,
`PATCH` and `DELETE` all produce the same fields for the same bytes:

- `application/json` — the decoded JSON array;
- `application/x-www-form-urlencoded` — the parsed form fields;
- `multipart/form-data` — the text parts (file parts land in `files` instead).

Any other media type yields `[]`: Bootgly never guesses how to parse a body whose type it
was not told. A request with no body yields `[]` as well, without touching the parser.

```php
$Request->fields; // Array ( [field_name] => field_value )
```

### Files

`files`: An associative array of files uploaded through the request. It replaces the
legacy `$_FILES` superglobal, and each record carries the same keys: `name` (the
sanitized file name), `type`, `size`, `error` (an `UPLOAD_ERR_*` constant) and
`tmp_name` (the temp file the streaming decoder wrote to disk).

```php
$Request->files; // Array ( [file_name] => file_attributes )
```

`type` is the part's `Content-Type` as the client sent it (surrounding spaces trimmed) — a hint, never proof:
anything can claim `image/png`. To accept a file by what it really is, validate it with the
[`MIME` rule](/guide/validation/overview/#validating-uploads), which sniffs the bytes.
`tmp_name` lives in `BOOTGLY_UPLOADS_DIR` (`BOOTGLY_STORAGE_DIR . 'temp/files/downloaded/'`), a folder
the server owns: persist an upload with `store()` before the request ends.

Each part's header block is parsed by the multipart grammar (RFC 7578): the
`Content-Disposition` parameters may come in any order, quoted or not, with or without a space
after `:` and `;`, and `Content-Type` may come before or after it. A part that breaks the
grammar makes the whole request a **`400 Bad Request`**, before any temp file is written for
it:

- no `Content-Disposition`, or one whose type is not `form-data`;
- a missing or empty `name`;
- a repeated `Content-Disposition`, `Content-Type`, `name` or `filename`;
- a parameter that breaks the list: whitespace around `=`, no value, or text after its value;
- a header line that is not a field (folded, without `:`, or with an invalid field name);
- a control byte in a header value, or an unterminated quoted string.

Inside a quoted value, `\"` and `\\` are escapes (the quoted-string grammar) and every other
backslash is kept, so a Windows path arrives whole. Browsers do not escape `\`: a name or
filename that *ends* in `\` reads as an unterminated string and is refused.

`filename*` (the RFC 5987 form) is ignored: a part that carries only `filename*` is a text
field.

An **empty file input** — a form submitted with no file chosen — still arrives as a
multipart part (`filename=""`), and decodes exactly as PHP's native parser would: the
record carries `error` = `UPLOAD_ERR_NO_FILE` (4) with empty `name`, `type` and
`tmp_name`, and **no temp file is created** for it. Gate on
`$file['error'] === UPLOAD_ERR_OK` before treating a record as an actual upload.

### Persist an upload (`store`)

`store()` moves a finished upload from its temp file into a [Storage](/guide/storage/overview/)
disk — Local, S3, or any registered driver — streaming the bytes (constant memory) and
removing the temp file on success.

```php
use Bootgly\ABI\Resources\Storage;

$Storage = new Storage([
   'disks' => ['uploads' => ['driver' => 's3', 'bucket' => 'assets', /* … */]],
]);

$Request->download();
$path = $Request->store('avatar', 'users/1/avatar.png', $Storage->open('uploads'));
// $path === 'users/1/avatar.png' on success, false otherwise ($Disk->error has the reason)
```

```php
public function store (string $key, string $path, Driver $Disk, array $options = []): string|false
```

Persists the uploaded file under `$key` into `$Disk` at `$path` (an empty `$path`, or one ending
in `/`, falls back to the uploaded file name). `$options` are forwarded to the driver's `write()`
(e.g. S3 `type`/`meta`). Returns the stored path, or `false` when the key is missing, the part
failed to upload, or the disk write failed — in which case the temp file is left for `clean()` to
reclaim and the reason is on `$Disk->error`.

## Metadata

`raw`: The raw HTTP request data.

```php
$Request->raw; // HTTP request data as string
```

`on`: The date on which the request was created.

```php
$Request->on; // '2020-03-10'
```

`at`: The time at which the request was created.

```php
$Request->at; // '17:16:18'
```

`time`: A Unix timestamp representing the time of request.

```php
$Request->time; // 1586496524
```

`secure`: Indicates if the request was made over HTTPS.

```php
$Request->secure; // true
```

## HTTP Authentication

```php
public function authenticate () : Basic|null;
```

This method extracts Basic authorization credentials from an HTTP Request. The Basic scheme match is case-insensitive as required by RFC 7235, so `Basic` and `basic` are both accepted. It only parses credentials; verification belongs to authentication guards and application resolvers. Bearer transport is exposed through `$Request->token` and handled by router authentication guards. Authentication parsing is cached per request and reset when the request is cloned or rebooted.

### Example of use

```php
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Request\Authentications\Basic;

$Credentials = $Request->authenticate();

if ($Credentials instanceof Basic) {
   $username = $Credentials->username;
   $password = $Credentials->password;

   // Verify username and password...
}

$token = $Request->token; // Bearer token for Router\Middlewares\Authentication\Bearer/JWT
```

### Generated metadata

`username`: The username provided in basic authentication.

```php
$Request->username; // 'bootgly'
```

`password`: The password provided in basic authentication.

```php
$Request->password; // 'example123'
```

`token`: The token provided in Bearer authentication.

```php
$Request->token; // 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...'
```

`identity`: The authenticated principal exposed by authentication guards.

```php
$Request->identity; // Bootgly\API\Security\Identity|string|null
```

`claims`: Verified token claims exposed by token-based guards.

```php
$Request->claims; // ['sub' => 'user-1', 'scope' => 'demo:read']
```

See [Authentication](/manual/WPI/HTTP/HTTP_Server_CLI/Authentication/) for guard-based Basic, Bearer, JWT, and Session examples.

## HTTP Content Negotiation

```php
public function negotiate (int $with = self::ACCEPTS_TYPES) : array;
```

The negotiate method is responsible for parsing the client's HTTP request headers and negotiating preferences regarding media types, languages, charsets, and encodings.

The negotiate method checks the relevant HTTP request header (such as `Accept`, `Accept-Language`, `Accept-Charset`, or `Accept-Encoding`) to retrieve the client's preferences. It then parses the values using regular expressions to extract the items and their respective qualities (if specified). The results are sorted by quality and returned as an array.

If the request header is empty or cannot be parsed, the method returns an empty array.

### Example of use

```php
// Assume a client makes a request to the server
// and the server receives the request object

// Negotiate the client's preferred language
$preferred_languages = $Request->negotiate(Request::ACCEPTS_LANGUAGES);

// Determine the best language to use based on server-supported languages
$available_languages = ['en', 'fr', 'de']; // Assume these are the languages the server supports

$selected_language = '';
foreach ($preferred_languages as $language => $quality) {
  if ( in_array($language, $available_languages) ) {
    $selected_language = $language;
    break;
  }
}

// Set the response language
if ( ! empty($selected_language) ) {
  // Set the response headers to indicate the selected language
  $Response->Header->set('Content-Language', $selected_language);
}

// Now, the server can generate a response in the selected language
// and send it back to the client
// ...
```

### Parameters

#### $with (optional)

An integer indicating the type of negotiation to be performed. Possible values are:

- `self::ACCEPTS_TYPES`: Negotiate media types (default).
- `self::ACCEPTS_LANGUAGES`: Negotiate languages.
- `self::ACCEPTS_CHARSETS`: Negotiate charsets.
- `self::ACCEPTS_ENCODINGS`: Negotiate encodings.

### Generated / availables metadata

`types`: The MIME type preferred by the HTTP Client in order of relevance.

```php
$Request->types; // Array ( [0] => 'text/html' [1] => 'text/plain' )
```

`type`: The MIME type most preferred by the HTTP Client.

```php
$Request->type; // 'text/html'
```

`languages`: The languages preferred by the HTTP Client in order of relevance.

```php
$Request->languages; // Array ( [0] => 'en-US' [1] => 'pt-BR' )
```

`language`: The language most preferred by the HTTP Client.

```php
$Request->language; // 'en-US'
```

`charsets`: The charsets preferred by the HTTP Client in order of relevance.

```php
$Request->charsets; // Array ( [0] => 'UTF-8' [1] => 'ISO-8859-15' )
```

`charset`: The charset most preferred by the HTTP Client.

```php
$Request->charset; // 'UTF-8'
```

`encodings`: The encodings preferred by the HTTP Client in order of relevance.

```php
$Request->encodings; // Array ( [0] => 'gzip' [1] => 'deflate' )
```

`encoding`: The encoding most preferred by the HTTP Client.

```php
$Request->encoding; // 'gzip'
```

### Notes

- This method is useful for servers that wish to provide content tailored to the client's preferences, such as sending the correct version of an image file based on the format accepted by the user's browser.
- Make sure to use the return values of this method according to your application's business logic, such as selecting the best response format based on the client's preferences.

## HTTP Caching

```php
public function freshen () : bool;
```

The freshen() method is responsible for determining whether a client request is considered "fresh" or not, based on certain headers and cache settings. This is useful for deciding whether to provide a completely new response or if a cached response can be used.

### Return

- Returns true if the request is considered fresh and can be served with a `cached` response.
- Returns false if the request is not considered fresh, and a `new response` should be generated.

### Evaluation Criteria

- The request must be of type `GET` or `HEAD`. Other request types are not considered fresh.
- The `Cache-Control` header should not contain the `no-cache` directive, indicating that the response should not be served from the cache.
- The `If-None-Match` (ETag) header is checked against the ETag of the cached response. If the ETags match, the cached response is considered valid.
- The `If-Modified-Since` header is compared with the `Last-Modified` header of the cached response. If the modification date of the response is more recent than the date specified in the `If-Modified-Since` header, the cached response is considered valid.

### Example with Last-Modified

```php
// Suppose the HTTP client request arrives like this...:

/*
GET / HTTP/1.1
Host: lab.bootgly.com:8080
User-Agent: insomnia/2023.4.0
If-Modified-Since: Fri, 14 Jul 2023 09:00:00 GMT
Accept: text/html

...
*/

$Response->Header->set('Last-Modified', 'Fri, 14 Jul 2023 08:00:00 GMT');

if ($Request->fresh) {
   return $Response(code: 304); // First onward is here
}
else {
   return $Response(body: 'test')->send(); // First Response here
}
```

### Generated metadata

`fresh`: Flag indicating if the request is to be considered fresh.

```php
$Request->fresh; // true
```

`stale`: Flag indicating if the request is to be considered stale.

```php
$Request->stale; // false
```

## Security Configuration

### Host Allowlist

```php
public static array $allowedHosts = [];
```

When non-empty, any request whose `Host` header (case-insensitive, port-agnostic) does not match an entry in the list is rejected with `400 Bad Request` at decode time — before any handler or middleware runs.

This blocks Host-header spoofing attacks such as cache poisoning and password-reset poisoning in multi-tenant applications (RFC 9112 §3.2 / §7.2).

Each entry is a **lowercase hostname without port**. Wildcard prefix `*.example.com` matches any single-label subdomain (`api.example.com`) but NOT the apex domain itself (`example.com`) and NOT multi-label subdomains (`a.b.example.com`). An empty list (the default) disables enforcement for backward compatibility — zero overhead on the hot path.

#### Example

```php
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Request;

// ? Allow only known hosts
Request::$allowedHosts = [
   'example.com',
   '*.example.com',  // matches api.example.com, app.example.com, etc.
   'localhost',
];
// @ Any request with a Host header not in this list receives 400 Bad Request
```

#### IPv6 support

IPv6 bracketed literals are handled correctly — the port portion after the closing `]` is stripped before matching:

```php
Request::$allowedHosts = [
   '[::1]',
   'localhost',
];
```

#### Disabling enforcement

```php
// : Reset to default — no enforcement
Request::$allowedHosts = [];
```

> **Note:** Set `$allowedHosts` before the server starts (e.g. in your bootstrap or project file). The value is inherited by all worker processes.

### Byte-range Limit

```php
public static int $maxRanges = 16;
```

Maximum number of members accepted in a single `Range` header. A request whose range set exceeds the limit is answered with `416 Range Not Satisfiable` (`Content-Range: bytes */<size>`) — the file is never read and no body is produced.

This bounds response amplification: without it, a few hundred bytes of `Range` header repeating the same range over and over would turn into one file read and one body copy **per member** (RFC 9110 §14.2). A 32-member set against an 82 KB file is already a ~2.6 MB response from a 261-byte header.

The limit is enforced before the set is parsed — the header is split at most `$maxRanges + 1` times — so an oversized set costs nothing beyond the check itself.

#### Example

```php
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Request;

// ? Tighten the limit on a static-file server
Request::$maxRanges = 4;
// @ `Range: bytes=0-9,20-29,40-49,60-69,80-89` (5 members) now receives 416 Range Not Satisfiable
```

#### Coalescing

Within the limit, `Response::upload()` combines the accepted set: overlapping and adjacent ranges are merged into a single part, keeping each merged part at its earliest requested position. Duplicated ranges therefore cannot multiply the response body.

```php
// @ `Range: bytes=0-9,5-14,15-19` → one part: `Content-Range: bytes 0-19/<size>`
```

When coalescing leaves a single range, the response is a plain `206 Partial Content` with a `Content-Range` header instead of a `multipart/byteranges` body.

#### Disabling byte ranges

Any value below `1` rejects every `Range` header with `416`:

```php
// : Refuse all range requests
Request::$maxRanges = 0;
```

> **Note:** Set `$maxRanges` before the server starts (e.g. in your bootstrap or project file). The value is inherited by all worker processes.

## Session

`Session`: the session object, lazy-initialized and persisted through the configured handler (cache-backed, `file` driver by default).

```php
$Request->Session; // Session object
```

Read and write it through the instance API — `get()`/`set()`, the bulk `put()`/`forget()`, `pull()`, `has()`/`check()`, `list()`, `flush()`, and `regenerate()` right after a privilege change. Every signature is in the [Reference](#reference) at the end of this page.

The server persists the session for you: once at the end of the synchronous cycle, right before the response is encoded, and — when the route deferred its response — again when the deferred work completes — on success, on error, at a handoff to SSE (before its wire is built) and at a handoff to a nested `defer()` (at the handoff itself). A cancelled deferral (the client left while it was parked) gets no save point of its own — the server does not persist it; the Session's own destructor is a safety net, though, so a write made before the client left can still reach storage later, when the cycle collector reclaims the abandoned generation. Inside deferred work reach the session through the snapshot — the closure's second argument, the same object as `$Response->Request` — as `$Request->Session`; a session first touched after the first `wait()` still emits its `Set-Cookie` on the deferred response when the work returns normally, never on an error answer.

When the route touched the Session before `defer()`, the deferral shares that Session object with the live request. If another request presenting the same cookie writes while the deferral is still parked, the deferred save meets a stale revision and is discarded rather than silently overwriting the newer write: the deferred response still answers, its Session write is simply lost, and the client keeps its cookie and the newer data — keep the writes of one session on one side of a parked deferral.

### Configuration

Session policy lives on the `Session` statics. Declare it at boot, before the server starts — the workers inherit the values:

```php
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Request\Session;

Session::$name = 'SID';                // cookie name (default `PHPSID`)
Session::$lifetime = 604800;           // seconds a record may stay idle before GC purges it
Session::$cookieLifetime = 604800;     // cookie Max-Age in seconds; 0 = browser-session cookie
Session::$cookiePath = '/';
Session::$domain = '';
Session::$autoUpdateTimestamp = true;  // sliding expiration: a read refreshes the record
Session::$gcProbability = [1, 20000];  // purge odds per finished request
Session::$secure = true;               // false only for HTTP-only development
Session::$httpOnly = true;
Session::$sameSite = 'Lax';            // or 'Strict'
```

A value you assign wins. `php.ini` — `session.gc_maxlifetime`, `session.gc_probability`/`session.gc_divisor` and the cookie params — fills only the statics still holding their declared default, so a policy written at boot is never replaced when the first session initializes; with an untouched `php.ini` that makes the cookie a browser-session one and takes PHP's GC odds. `$secure`, `$httpOnly` and `$sameSite` are never read from `php.ini`. A static assigned after the first session takes effect on the next cookie or purge.

<span id="request-validation"></span>

## Request Validation

Bootgly ships a fluent validation system for verifying request data before your handler runs. It revolves around three pieces:

- [`Bootgly\ADI\Validation`](/guide/validation/overview/) — the standalone engine: runs a set of rules against an array of input data, accumulating errors per field. It lives in the ADI layer, so the same rules validate CLI input, jobs and seeders.
- `Bootgly\ADI\Validators\*` — built-in rule classes (`Required`, `Boolean`, `Integer`, `Minimum`, `Maximum`, `In`, `Email`, `URL`, `Date`, `Confirmed`, `Regex`, `Size`, `MIME`, `Extension`).
- `Validator` middleware — applies validation to one Request source and fails fast (default `422 Unprocessable Entity`) if invalid. See [Middlewares → Validator](/manual/WPI/HTTP/HTTP_Server_CLI/Middlewares/#validator).

### Standalone Validation

Run validation directly on any associative array — useful when you need access to the validation result inside a handler:

```php
use Bootgly\ADI\Validation;
use Bootgly\ADI\Validators\Email;
use Bootgly\ADI\Validators\Integer;
use Bootgly\ADI\Validators\Maximum;
use Bootgly\ADI\Validators\Minimum;
use Bootgly\ADI\Validators\Required;

$Validation = new Validation(
   source: $Request->fields,
   rules: [
      'email' => [new Required, new Email],
      'age'   => [new Required, new Integer, new Minimum(18), new Maximum(120)],
   ]
);

$Validation->valid;  // true | false
$Validation->errors; // ['email' => ['email must be a valid email address.'], ...]
```

Errors are stored as `array<field, array<string>>` — a single field can accumulate multiple messages (one per failed rule). The full engine reference — optional/implicit semantics, custom messages and non-HTTP recipes — lives in the [Validation guide](/guide/validation/overview/).

### Available Sources

The `Sources` enum identifies which Request property the `Validator` middleware reads:

| Source | Request property | Description |
|---|---|---|
| `Sources::Fields` | `$Request->fields` | Parsed form fields / decoded body |
| `Sources::Queries` | `$Request->queries` | Query-string parameters |
| `Sources::Headers` | `$Request->headers` | HTTP request headers |
| `Sources::Cookies` | `$Request->cookies` | Request cookies |
| `Sources::Files` | `$Request->files` | Uploaded file structures |

```php
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Validator\Sources;
```

Header field names are normalized to lowercase by the parser, but `Sources::Headers` rules match them case-insensitively (RFC 9110) — key your rules `'X-API-Key'` or `'x-api-key'`, both bind. Every other source matches rule keys case-sensitively.

With `Sources::Cookies`, rules bind by cookie name; when the same name appears on multiple `Cookie` lines, the first line wins — matching `Cookies::get()`.

### Built-in Validators

All built-in rules live in `Bootgly\ADI\Validators` — `Required`, `Boolean`, `Integer`, `Minimum`, `Maximum`, `In`, `Email`, `URL`, `Date`, `Confirmed`, `Regex`, `Size`, `MIME` and `Extension`. Each accepts an optional `string $message` constructor argument to override the default error message. The one-block-per-rule catalog (arguments, semantics and default messages) lives in the [Validation guide](/guide/validation/overview/).

### Custom Rules

Extend `Bootgly\ADI\Validators` and implement `validate()` (returns `true` if valid) and `format()` (returns the error message):

```php
use Bootgly\ADI\Validators;

$InviteCode = new class extends Validators {
   /**
    * @param array<string,mixed> $data  Full source array — useful for cross-field rules.
    */
   public function validate (string $field, mixed $value, array $data): bool
   {
      return is_string($value) && $value === 'bootgly';
   }

   public function format (string $field): string
   {
      return "{$field} must match the demo invite code.";
   }
};
```

Set `$implicit = true` in your subclass when the rule must run even for missing/blank fields (the way `Required` does).

### Validator Middleware

To plug validation directly into a route, use the `Validator` middleware. The handler is skipped if any rule fails:

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

yield $Router->route('/users', function (Request $Request, Response $Response) {
   return $Response->JSON->send(['created' => true, 'user' => $Request->fields]);
}, POST, middlewares: [
   new BodyParser,
   new Validator(rules: [
      'email' => [new Required, new Email],
   ], Source: Sources::Fields),
]);
```

See [Middlewares → Validator](/manual/WPI/HTTP/HTTP_Server_CLI/Middlewares/#validator) for the full middleware reference (status code, fallback closure).

### End-to-End Example

A complete router showcasing all validation modes — body, query string, file upload, custom rule, and a custom failure response:

```php
use Bootgly\ADI\Validation;
use Bootgly\ADI\Validators;
use Bootgly\ADI\Validators\Email;
use Bootgly\ADI\Validators\Extension;
use Bootgly\ADI\Validators\Integer;
use Bootgly\ADI\Validators\Maximum;
use Bootgly\ADI\Validators\MIME;
use Bootgly\ADI\Validators\Minimum;
use Bootgly\ADI\Validators\Regex;
use Bootgly\ADI\Validators\Required;
use Bootgly\ADI\Validators\Size;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Request;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Response;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\BodyParser;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Validator;
use Bootgly\WPI\Nodes\HTTP_Server_CLI\Router\Middlewares\Validator\Sources;

$Custom = new class extends Validators {
   public function validate (string $field, mixed $value, array $data): bool
   {
      return is_string($value) && $value === 'bootgly';
   }
   public function format (string $field): string
   {
      return "{$field} must match the demo invite code.";
   }
};

// Fail-closed body validation
yield $Router->route('/validation/middleware', function (Request $Request, Response $Response) {
   return $Response->JSON->send(['created' => true, 'fields' => $Request->fields]);
}, POST, middlewares: [
   new BodyParser,
   new Validator(rules: [
      'email' => [new Required, new Email],
      'age'   => [new Required, new Integer, new Minimum(18), new Maximum(120)],
   ], Source: Sources::Fields),
]);

// Custom failure response
yield $Router->route('/validation/fallback', function (Request $Request, Response $Response) {
   return $Response->JSON->send(['created' => true]);
}, POST, middlewares: [
   new BodyParser,
   new Validator(
      rules: ['email' => [new Required, new Email]],
      Source: Sources::Fields,
      fallback: function (Request $Request, Response $Response, Validation $Validation): Response {
         $Response->code(400);
         return $Response->JSON->send([
            'created' => false,
            'fields'  => $Request->fields,
            'errors'  => $Validation->errors,
         ]);
      }
   ),
]);

// Query validation
yield $Router->route('/validation/query', function (Request $Request, Response $Response) {
   return $Response->JSON->send(['queries' => $Request->queries]);
}, GET, middlewares: [
   new Validator(rules: [
      'page'   => [new Integer, new Minimum(1)],
      'filter' => [new Regex('/\A[a-z0-9_-]+\z/')],
   ], Source: Sources::Queries),
]);

// File upload validation
yield $Router->route('/validation/files', function (Request $Request, Response $Response) {
   return $Response->JSON->send(['files' => $Request->files]);
}, POST, middlewares: [
   new BodyParser,
   new Validator(rules: [
      'avatar' => [
         new Required,
         new Size(2 * 1024 * 1024),
         new MIME(['image/jpeg', 'image/png']),
         new Extension(['jpg', 'jpeg', 'png']),
      ],
   ], Source: Sources::Files),
]);

// Custom rule
yield $Router->route('/validation/custom', function (Request $Request, Response $Response) {
   return $Response->JSON->send(['accepted' => true]);
}, POST, middlewares: [
   new BodyParser,
   new Validator(rules: [
      'code' => [new Required, $Custom],
   ], Source: Sources::Fields),
]);
```

## Reference

### Session instance API

`$Request->Session` is a `Bootgly\WPI\Nodes\HTTP_Server_CLI\Request\Session`. Its payload lives in the object, never in `$_SESSION`, and the server persists it for you (see [Session](#session)) — a handler only reads and writes. Two properties are publicly readable: `$id`, the current session ID, and `$loaded`, `true` only when the ID presented by the cookie matched a server-issued record that was read back. Every mutator below also appends the session `Set-Cookie` to the current response, once; reading never does.

```php
public function get (string $name, mixed $default = null): mixed
```

Reads one value, or `$default` when the key is absent. The lookup is `??`-based, so a stored `null` yields `$default` too.

```php
public function set (string $name, mixed $value): void
```

Stores one value and marks the session dirty for the next save.

```php
public function put (array|string $key, mixed $value = null): void
```

Bulk store. An `array<string, mixed>` merges every pair in one call; a `string` key delegates to `set()` with `$value`.

```php
public function pull (string $name, mixed $default = null): mixed
```

Reads a value and deletes it in the same call — a `get()` followed by a `delete()`. Returns `$default` when the key is absent.

```php
public function delete (string $name): void
```

Removes one key.

```php
public function forget (array|string $name): void
```

Bulk remove. An `array<int, string>` unsets every listed key in one call; a scalar delegates to `delete()`.

```php
public function has (string $name): bool
```

`true` when the key exists **and** its value is not `null` — it is an `isset()`.

```php
public function check (string $name): bool
```

`true` when the key exists, even when its value is `null` — it is an `array_key_exists()`. Use it to tell a stored `null` from a key that was never written.

```php
public function list (): array
```

Returns the whole payload as an `array<string, mixed>`.

```php
public function flush (): void
```

Drops every key. An emptied session is destroyed at the next save instead of being written back empty.

```php
public function regenerate (): void
```

Rotates the session ID — call it immediately after authenticating a user or elevating privileges. The old record is destroyed, a fresh cryptographically random ID is generated, the payload is migrated and the `Set-Cookie` of the current response is updated. When another request already changed or revoked the loaded snapshot, the stale payload is discarded rather than migrated into the new ID. Throws `RuntimeException` when a loaded session's handler does not implement the atomic commit/revoke contract.

```php
public function save (): void
```

Persists the session through the configured handler, or destroys the record when `flush()` emptied it. The server already calls it at every save point of the request cycle, so a handler rarely needs to; it returns immediately when nothing changed or no handler is configured.
