> ## Documentation Index
> Fetch the complete documentation index at: https://docs.instructorphp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 5 streaming responses

Streaming responses let you process data as it arrives from the server rather than waiting for the entire response to buffer in memory. This is particularly valuable when working with LLM APIs that generate tokens incrementally, downloading large files, or consuming real-time event streams.

Streams are considered complete only after their iterator reaches the natural end. Breaking early leaves a stream incomplete; one-shot streams cannot then be replayed. When constructing a response from an iterable, use `HttpResponse::streamingFromIterable()` for non-buffering consumption or `HttpResponse::bufferedFromIterable()` when replay is explicitly required. Passing a raw iterable to `HttpResponse` is rejected so it cannot silently retain every chunk.

## Enabling Streaming

To receive a streaming response, set the `stream` option on the request:

```php theme={null}
use Cognesy\Http\Data\HttpRequest;

$request = new HttpRequest(
    url: 'https://api.example.com/stream',
    method: 'GET',
    headers: ['Accept' => 'text/event-stream'],
    body: '',
    options: ['stream' => true],
);
// @doctest id="c5f9"
```

You can also enable streaming on an existing request using `withStreaming()`:

```php theme={null}
$request = $request->withStreaming(true);
// @doctest id="3485"
```

## Consuming the Stream

Once you have a streaming request, call `stream()` on the pending response. This returns a PHP Generator that yields string chunks:

```php theme={null}
foreach ($client->send($request)->stream() as $chunk) {
    echo $chunk;
    flush();
}
// @doctest id="800d"
```

Each chunk is a raw string as received from the transport layer. Its size depends on the driver and on the `streamChunkSize` setting in `HttpClientConfig` (default: 16384 bytes), which is an upper bound rather than a target: chunks are yielded as soon as the transport delivers them, so a chunk is usually far smaller than the limit and lowering the limit does not make data arrive sooner. Do not depend on chunk boundaries — parse the stream, not the framing.

> **Note:** You do not need to explicitly set `stream => true` on the request when using `PendingHttpResponse::stream()`. The pending response will force streaming mode automatically. However, setting it on the request is useful when middleware needs to know the intended mode before execution.

## Streaming LLM Responses

Streaming is essential for AI/LLM integrations where responses are generated token by token. Here is a typical pattern for streaming a chat completion:

```php theme={null}
$request = new HttpRequest(
    url: 'https://api.openai.com/v1/chat/completions',
    method: 'POST',
    headers: [
        'Content-Type' => 'application/json',
        'Authorization' => 'Bearer ' . $apiKey,
    ],
    body: [
        'model' => 'gpt-4',
        'messages' => [
            ['role' => 'user', 'content' => 'Write a haiku about PHP.'],
        ],
        'stream' => true,
    ],
    options: ['stream' => true],
);

foreach ($client->send($request)->stream() as $chunk) {
    echo $chunk;
    flush();
}
// @doctest id="71ba"
```

The raw chunks from the transport layer will contain server-sent event framing (e.g., `data: {...}\n\n`). To parse these into clean payloads, use the `EventSourceMiddleware`.

## Server-Sent Events with EventSourceMiddleware

The `EventSourceMiddleware` handles the SSE protocol for you. It strips the `data:` prefixes, buffers partial lines, and yields complete event payloads. A final event is emitted when the source ends even if it has no trailing blank line. The parser buffer is limited to 1 MiB by default; configure `maxBufferBytes` on `EventSourceMiddleware` for a smaller or larger protocol-specific limit:

```php theme={null}
use Cognesy\Http\Extras\Middleware\EventSource\EventSourceMiddleware;

$client = $client->withMiddleware(
    (new EventSourceMiddleware(true))
        ->withParser(fn(string $payload): string => $payload),
    'eventsource',
);
// @doctest id="1922"
```

The parser callback receives the raw payload string from each `data:` line and returns the value to yield. Return `false` to skip an event. This is useful for filtering out `[DONE]` markers or parsing JSON:

```php theme={null}
$client = $client->withMiddleware(
    (new EventSourceMiddleware(true))
        ->withParser(function (string $payload): string|bool {
            if ($payload === '[DONE]') {
                return false; // skip
            }
            return $payload;
        }),
    'eventsource',
);
// @doctest id="f579"
```

You can also attach listeners for debugging or event dispatching:

```php theme={null}
use Cognesy\Http\Extras\Support\EventSource\Listeners\PrintToConsole;
use Cognesy\Http\Config\DebugConfig;

$middleware = (new EventSourceMiddleware(true))
    ->withListeners(new PrintToConsole(new DebugConfig(httpEnabled: true)))
    ->withParser(fn(string $payload): string => $payload);
// @doctest id="c35b"
```

## Downloading Large Files

Streaming is the right approach for downloading large files without exhausting memory:

```php theme={null}
$request = new HttpRequest(
    url: 'https://example.com/large-dataset.csv',
    method: 'GET',
    headers: [],
    body: '',
    options: ['stream' => true],
);

$handle = fopen('dataset.csv', 'wb');

foreach ($client->send($request)->stream() as $chunk) {
    fwrite($handle, $chunk);
}

fclose($handle);
// @doctest id="0872"
```

## Capturing stream contents for inspection

Streamed bodies are consumed once and normally leave no trace. When you need to
inspect what actually came over the wire — debugging a malformed SSE stream,
building replay tooling, capturing a postmortem sample — enable opt-in capture
on the response before consuming it:

```php theme={null}
use Cognesy\Http\Stream\StreamCapturingPolicy;

$response = $client->send($request)
    ->get()
    ->withStreamCapture(StreamCapturingPolicy::preview()); // first 64KB

foreach ($response->stream() as $chunk) {
    // process chunks as usual — capture happens transparently
}

$capture = $response->streamCapture();
echo $capture->preview();          // captured prefix of the raw stream
$stats = $capture->stats();        // bytes, chunks, capturedBytes, truncated
// @doctest id="6323"
```

Policies bound memory explicitly:

* `StreamCapturingPolicy::preview(int $maxBytes = 65536)` — capture a prefix,
  enough to see what the stream looked like
* `StreamCapturingPolicy::chunks(int $maxBytes = 1048576)` — retain individual
  chunks (via `capturedChunks()`) up to the byte budget
* `StreamCapturingPolicy::full(int $maxBytes)` — capture everything up to an
  explicit cap (`capturedBody()` returns the concatenated content)
* `StreamCapturingPolicy::disabled()` — pass-through, zero retention

Capture is per-response and off by default; it never changes what the consumer
of `stream()` sees. The `truncated` flag in `stats()` tells you when the byte

The default is intentionally disabled because capture adds per-chunk work and
retention to a path designed to stay one-shot. With 50,000–100,000 small chunks,
`chunks()`/`full()` can retain 50,000–100,000 PHP string and array entries before
their byte cap is reached; the object overhead can be several times larger than
the payload itself. `preview()` retains only one bounded prefix, while
`disabled()` avoids capture storage entirely. Choose a small preview for
diagnostics on long-lived streams and enable full/chunk capture only when the
retained data is explicitly needed.

## Considerations

When working with streaming responses, keep these points in mind:

* **Memory usage.** Streaming avoids buffering the entire response, but be careful not to accumulate chunks in a variable unless you actually need the full content.
* **Connection stability.** Streaming connections stay open longer and are more sensitive to network interruptions. Pair streaming with retry middleware for resilience.
* **Timeouts.** The `idleTimeout` setting in `HttpClientConfig` controls how long the client waits between data packets. Set it to `-1` to disable idle timeouts for long-lived streams.
* **Body access.** Calling `body()` on a streamed `HttpResponse` throws a `LogicException`. Always use `stream()` for streamed responses.
* **Middleware order.** Middleware that decorates the stream (like `EventSourceMiddleware`) should be registered before middleware that reads the final content.
