# HTTP Server CLI — Response

## Overview

The `Response` object is automatically available in every route handler of the HTTP Server CLI. It provides an easy-to-use API for managing HTTP responses — configuring statuses, headers, and body content, as well as facilitating view rendering, file uploads, authentication, and redirection.

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

## Usage

Below are the methods provided by the `Response` object with examples demonstrating their usage.

### Invocation

```php
public function __invoke (int $code = 200, array $headers = [], string $body = '') : self;
```

**Description:**

This magic method allows the Response object to be invoked as a function, resetting the response with the provided parameters.

**Example:**

```php
return $Response(404, ['Content-Type' => 'text/plain'], 'Not Found');
```

### Append body

```php
public function append ($body);
```

**Description:**

Appends data to the response body.

**Parameters:**

- `$body` (mixed): Data to append to the response.

**Example:**

```php
return $Response->append('Additional information');
```

### Render a view

```php
$Response->View->render(string $view, null|array $data = null, null|Closure $callback = null): Response;
```

**Description:**

Renders a project view through the built-in `View` response resource and appends it to the
response body.

**Parameters:**

- `$view` (string): The view to render.
- `$data` (array|null, optional): Data to pass to the view.
- `$callback` (Closure|null, optional): An additional callback executed after rendering the view.

**Example:**

```php
return $Response->View->render('welcome', ['title' => 'Welcome Page']);
```

### Send content

```php
public function send ($body = null, ...$options) : self;
```

**Description:**

Finalizes the response by setting the body content and sending the response to the client.

**Parameters:**

- `$body` (mixed|null, optional): Optional body content to send.
- `...$options` (mixed): Additional options that may be passed, specifics depend on implementation.

**Examples:**

```php
return $Response->send('{"status":"success"}');
```

```php
return $Response->JSON->send(['Hello' => 'World!']);
```

### Deferred I/O resources

Use `defer()` for response work that must wait for external I/O. Response Resources loaded
with `Response\Configs(Resources:)` can bridge that I/O to the response scheduler, but the response is
still finalized with the normal `send()` flow.

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

return $Response->defer(function (Response $Response): void {
   $Database = $Response->Database;
   $Result = $Database->fetch('SELECT 1 AS ok');

   $Response->JSON->send([
      'rows' => $Result->rows,
   ]);
});
```

See **[Response Resources](/manual/WPI/HTTP/HTTP_Server_CLI/Response/Resources/)** for built-in resources and the DBAL bridge.

### Upload files

```php
public function upload (string $file, int $offset = 0, null|int $length = null, bool $close = true) : self;
```

**Description:**

Sends the contents of a file to the HTTP client **as a download**. The file is streamed from disk in chunks — it is never loaded into memory whole — so `upload()` is the method for files of any size. ("Upload" is named from the server's point of view: the server uploads the file to the client.)

A whole-file (or single-range) response carries `Content-Type: application/octet-stream` and `Content-Disposition: attachment; filename="<basename>"`, so browsers save the file instead of displaying it. To show a file's contents in the browser, see [Send file contents inline](#send-file-contents-inline) below. Every `upload()` response also carries `Last-Modified`, `Cache-Control: no-cache, must-revalidate` and `Expires: 0` — and `Accept-Ranges: bytes` when the whole file is sent.

**How the bytes move:** each slice is read by PHP and written to the non-blocking connection — up to 1 MiB at a time on HTTP/1.1, sized by the stream's flow-control window on HTTP/2 — and the worker keeps serving its other connections while a slow client drains. There is no `sendfile(2)` zero-copy path: PHP has no binding to that system call. [Static Files](/manual/WPI/HTTP/HTTP_Server_CLI/#static-files) in the HTTP Server CLI manual covers when to put a CDN or a reverse proxy in front.

**Parameters:**

- `$file` (string): The file path, **relative to the project directory** (`BOOTGLY_PROJECT->path`). A leading `/` is still read from the project directory, and a path that resolves outside it answers `403 Forbidden`.
- `$offset` (int): The byte offset where the data starts.
- `$length` (int|null): How many bytes to send (`null` = up to the end of the file).
- `$close` (bool): Close the connection after sending.

**Example 1 — the whole file:**

```php
return $Response->upload('storage/files/report.pdf');
```

**Example 2 — a window of the file (the first 2 bytes, answered as `206 Partial Content`):**

```php
return $Response->upload('statics/alphanumeric.txt', offset: 0, length: 2);
```

**Byte ranges:**

A client `Range` header overrides `$offset` / `$length`. The accepted set is coalesced — overlapping and adjacent ranges are merged — and a set larger than [`Request::$maxRanges`](/manual/WPI/HTTP/HTTP_Server_CLI/Request/#byte-range-limit) (16 by default) is rejected with `416 Range Not Satisfiable`. A `Range` value that is not even a ranges-specifier — no `=` separator at all, e.g. `Range: bytes 0-1` — is rejected with `400 Bad Request` (RFC 9110 §14.2 allows a server to ignore or reject it; Bootgly rejects).

**Representation identity:**

`upload()` resolves the path, opens it once and records the identity of what it opened — device, inode, mode, size, modification time and change time — then closes the descriptor. Nothing is kept open while the response waits on a slow client, so the server reopens the file for each chunk it writes and checks that identity again every time, on both HTTP/1 and HTTP/2.

If the file no longer matches, the response is refused rather than completed with the wrong bytes: on HTTP/1 the connection is closed (there is no way to retract a `Content-Length` already sent), and on HTTP/2 the stream is reset. A warning naming the path and the field that changed is logged.

Two consequences are yours to plan for:

- **Do not serve a file while it is being written.** Any change — an append, a rewrite, an atomic redeploy, even a `chmod` or `chown` that only moves the change time — aborts a transfer already in flight. `stat` cannot tell a harmless append from a substitution, so the check cannot make an exception for one. Snapshot the file first:

  ```php
  $Router->route('/logs/download', function ($Request, $Response) {
     $snapshot = 'storage/snapshots/' . bin2hex(random_bytes(8)) . '.log';
     copy(BOOTGLY_PROJECT->path . 'storage/logs/app.log', BOOTGLY_PROJECT->path . $snapshot);

     return $Response->upload($snapshot);
  });
  ```

- **Serve from a filesystem with stable inodes.** Mounts that regenerate `st_dev` / `st_ino` between two opens of the same path — some FUSE-backed remote filesystems, an overlay copy-up triggered mid-transfer — are indistinguishable from a substitution and will abort. Local filesystems (ext4, XFS, Btrfs, tmpfs) are fine.

Symbolic links are supported: the link is resolved once, while the path is being checked against the project jail, and the file it pointed to at that moment is what gets served. Repointing the link afterwards does not redirect a response already under way.

**The jail confines paths, not inodes:**

`upload()` rejects any path that resolves outside `BOOTGLY_PROJECT->path`. That check is about *names*. A hard link has no target to resolve — it is simply a second name for one inode — so a hard link created inside the served tree resolves to a path inside the jail and is served, whatever the file's other name points at:

```sh
ln /srv/app/.env  /srv/app/public/uploads/notes.txt   # now servable
```

Linux blocks the interesting half of this by default: with `fs.protected_hardlinks=1` (the default on current kernels) you may only hard-link a file you own or can already read, so this cannot be used to reach a file you were denied. What remains is a change of *exposure* — code running as the application can turn a file it could already read locally into one the world can fetch.

So: **do not let less-trusted code write into a served tree.** A directory that accepts uploads should not be the same directory a route serves back, and neither should sit next to application secrets. If you cannot separate them, serve uploads through a handler that reads and returns the bytes itself, rather than through `upload()`.

### Send file contents inline

`upload()` marks the file as a download. To have the browser display it instead — a PDF, an image, a text file — call `upload()` and then replace the two headers it set. The file is still streamed from disk, never loaded into memory:

```php
$Router->route('/terms', function (Request $Request, Response $Response) {
   $Response->upload('statics/terms.txt');

   // ? Only a single-body answer (the whole file or one range) carries these headers
   if ($Response->Header->get('Content-Disposition') !== '') {
      $Response->Header->set('Content-Type', 'text/plain; charset=utf-8');
      $Response->Header->set('Content-Disposition', 'inline');
   }

   return $Response;
}, GET);
```

Headers are serialized only after the handler returns, so values set after `upload()` replace the ones it wrote. Keep the `Content-Disposition` check: when a client asks for several ranges at once, `upload()` answers `206` with `Content-Type: multipart/byteranges; boundary=…` and sets no `Content-Disposition`. Overwriting that `Content-Type` would break the multipart body. The check also skips error answers (`403`, `416`, …), which set neither header.

In a Web platform project, [`Statics`](/manual/Web/App/overview/) serves a whole folder of assets inline, with the media type mapped from the extension — it reads each file into memory, so keep this recipe for large files. [Static Files](/manual/WPI/HTTP/HTTP_Server_CLI/#static-files) in the HTTP Server CLI manual compares both paths.

> Looking for `serve()`? It is not a Response method and it does not read files: [`Router::serve()`](/manual/WPI/HTTP/HTTP_Server_CLI/Router/#fixed-body-routes) registers a route that always answers with a fixed string you pass at registration.

### HTTP Authentication

```php
public function authenticate (Authentication $Method) : self
```

**Description:**

Sends an authentication challenge to the client, typically in response to a protected resource being accessed without proper credentials.

**Parameters:**

- `$Method` (Authentication): A HTTP Authentication Method. Supported method: Basic.

**Basic example:**

```php
use Bootgly\WPI\Modules\HTTP\Server\Response\Authentication;

return $Response
   ->authenticate(new Authentication\Basic(realm: "Bootgly Protected Area"));
```

Bearer challenges are emitted by `Router\Middlewares\Authentication\Bearer` and `Router\Middlewares\Authentication\JWT`. See [Authentication](/manual/WPI/HTTP/HTTP_Server_CLI/Authentication/) for middleware guards and challenge details.

### Redirect to new URI

```php
public function redirect (string $URI, int|null $code = null, bool $allowExternal = false) : self;
```

**Description:**

Redirects the client to a new URI.

By default, **external URLs are blocked** to prevent open redirect vulnerabilities. If the `$URI` starts with `http://`, `https://`, or `//`, it is replaced with `/` unless `$allowExternal` is explicitly set to `true`.

**Parameters:**

- `$URI` (string): The URI to redirect to.
- `$code` (int|null, optional): HTTP status code for the redirection. Defaults to 307 for GET or 303 for POST redirects.
- `$allowExternal` (bool, default `false`): Whether to allow redirects to external URLs. When `false`, absolute URLs with a scheme are rejected and replaced with `/`.

**Examples:**

Internal redirect (relative URI):

```php
return $Response->redirect('/dashboard');
```

Internal redirect with specific status code:

```php
return $Response->redirect('/new-location', 301);
```

External redirect (must opt-in with `allowExternal: true`):

```php
return $Response->redirect('https://docs.bootgly.com/', allowExternal: true);
```

External redirect with specific status code:

```php
return $Response->redirect('https://docs.bootgly.com/', 302, allowExternal: true);
```

> ⚠️ **Security:** Never pass user-supplied input directly to `redirect()` without validation. If the redirect target may be controlled by the user (e.g., a `?next=` parameter), validate that the URI is relative or matches an allowed host before calling `redirect()`.

### Terminate the HTTP Response

```php
public function end (null|int $code = null, null|string $context = null) : self;
```

**Description:**

Definitively terminates the HTTP response. When `$code` is given, it is set as the response status exactly as passed — headers and body already set by the handler are kept as-is.

`416 Range Not Satisfiable` is the one preset code: `end(416, (string) $size)` cleans the headers already set, ships a minimal one-space body and — when `$context` is given — emits `Content-Range: bytes */{$context}`.

**Parameters:**

- `$code` (int|null, optional): The status code to send before ending the response.
- `$context` (string|null, optional): Context for preset codes — for `416`, the representation size used to build the `Content-Range` header. Ignored for every other code.

## Deferred Responses (Async)

```php
public function defer (Closure $work, int|float $timeout = 0): Response;
```

Executes `$work` asynchronously via a PHP Fiber, allowing the event loop to handle other connections while this response is being prepared.

Inside `$work()`, call `$Response->wait()` to yield control back to the event loop:

- **Wait with `null`** → the Fiber resumes on the next event loop tick (tick-based scheduling).
- **Wait with a `resource`** → the Fiber resumes when `stream_select()` detects I/O readiness on that resource (I/O-bound scheduling).

The response is sent automatically when `$work()` returns. If the work throws, the route's middlewares that implement `Recovering` are asked to answer — innermost first, then the global pipeline (see *Error Boundaries and Deferred Work* on the [Middlewares](/manual/WPI/HTTP/HTTP_Server_CLI/Middlewares) page); when none does, a `500 Internal Server Error` is returned (`503 Service Unavailable` for a `Response\Timeout`). The boundaries are the chain of the route this request was dispatched with; a deferral started after the pipeline returned (a `Request\Events::Handled` listener, a global middleware after its `$next()`) only reaches the global pipeline, and work that already handed off (an open SSE stream, a nested `defer()`) reaches no boundary at all.

**What the work sees.** `defer()` calls your closure as `function (Response $Response, Request $Request)`: the `Response` is a private clone for this deferral and the `Request` is a *snapshot* of the request taken at the moment of the call — the same object as `$Response->Request`, which is how you reach it from anywhere that only has the `Response` (a nested `defer()`, a resource). The snapshot is handed only to a second parameter that accepts a `Request` (typed `Request`, or untyped and required); a closure that declares an optional second parameter of another type keeps that parameter's default. Method, URI, fields, uploaded files, authentication, identity, claims, attributes and the Session survive every `wait()`. Route parameters live on the Route, not on the request: `defer()` re-binds the matched Route for every segment of the work, so `WPI->Router->Route->Params` keeps answering for this request. Read the request only through the closure parameters — never through a copy of the route's `$Response` kept in a `use ()` or on `$this`: that object is the worker's shared response, and once your work parks its `$Request` is whichever request the worker is serving now, cookies and Session included. The `$Request` your route received is the live per-connection object: the server empties its body and fields as soon as the synchronous cycle ends and reuses it for the next request on that connection, so a `use ($Request)` inside deferred work reads empty fields after the first `wait()` — and its URI, cookies and Session are not emptied: they may already describe a later request. Headers a middleware sets after `$next()` do not apply to a deferred response — the pipeline has returned by then; set them inside the work.

A Session written inside deferred work is persisted when the deferral answers — on success, on error, when it hands off to SSE (right before the wire leaves) and when it hands off to a nested `defer()` (at the handoff itself, whether or not the child ever answers) — and a session first touched after a `wait()` sets its cookie on the deferred response when the work returns normally. The server's own error answer never carries that cookie: `Catcher` builds the `500`/`503` on a fresh `Response`, exactly as it does for a route that throws synchronously, so a session created by a request that fails is persisted under an ID the client is never told — a `Recovering` middleware answering in place on the deferred `Response` keeps it. A cancelled deferral (the client left while it was parked) gets no save point of its own — the server does not persist it. Do not read that as a discard: the Session's own destructor is a safety net, so a write made before the client left can still reach storage later, when the cycle collector reclaims the abandoned generation; when an abandoned request must leave nothing behind, write the Session in the last segment of the work — after the last `wait()` and after any handoff: a write made before a nested `defer()` is persisted at that handoff, so it must go inside the child instead.

`$timeout` bounds, in seconds, how long the work may stay parked (`0` uses the server-wide `deferredTimeout`, where `0` means unbounded): when it elapses a `Response\Timeout` is thrown at the wait point — catch it to answer yourself, let a `Recovering` middleware answer it, or let it answer `503 Service Unavailable`. A parked deferral is never cut by the connection idle reaper. See *Deferred Response Lifecycle* on the [HTTP Server CLI](/manual/WPI/HTTP/HTTP_Server_CLI) page.

### Tick-based example

Useful for CPU-bound work that should not block other connections:

```php
yield $Router->route('/defer/tick', function ($Request, Response $Response) {
   return $Response->defer(function (Response $Response): void {
      $partial = '';
      for ($i = 1; $i <= 5; $i++) {
         $partial .= "chunk {$i}\n";
         $Response->wait(); // Resume on next tick
      }
      $Response->send($partial);
   });
}, GET);
```

### I/O-aware example

Useful for waiting on external resources (databases, APIs, sockets):

```php
yield $Router->route('/defer/io', function ($Request, Response $Response) {
   return $Response->defer(function (Response $Response): void {
      [$read, $write] = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
      stream_set_blocking($read, false);

      // Simulate async I/O: write in a non-blocking way
      fwrite($write, 'Hello from async I/O!');
      fclose($write);

      // Suspend until the read socket has data
      $Response->wait($read);

      $data = stream_get_contents($read);
      fclose($read);

      $Response->send($data);
   });
}, GET);
```

### wait()

```php
public function wait (mixed $value = null): Response;
```

Yields control back to the event loop from inside a `defer()` closure. The behavior depends on the value passed:

- **`$Response->wait()`** — tick-based: the Fiber resumes on the next event loop iteration.
- **`$Response->wait($stream)`** — I/O-bound: the Fiber resumes when `stream_select()` detects readiness on the given stream resource.
- **`$Response->wait($Readiness)`** — explicit I/O-bound scheduling through a DBAL or event-loop readiness object.

If called outside of a deferred context (`$this->deferred === false`), the method returns immediately with no effect.

**Parameters:**

- `$value` (Readiness|resource|null, default `null`): A readiness request, PHP stream resource, or `null` for tick-based scheduling.
