---
title: "Laravel Octane 2.23: стрімінг відповідей-генераторів запрацював на FrankenPHP і Swoole"
url: https://laravelukraine.com/blog/laravel-octane-223-striming-vidpovidei-generatoriv-zapraciuvav-na-frankenphp-i-swoole
date: 2026-11-14
---

# Laravel Octane 2.23: стрімінг відповідей-генераторів запрацював на FrankenPHP і Swoole

[Laravel Octane 2.23.0](https://github.com/laravel/octane/releases/tag/v2.23.0) виправив баг, який ламав стрімінг під Octane: `response()->stream()` з генератором на FrankenPHP і Swoole повертав `200` з порожнім тілом. Саме так ламалися SSE-потоки і стрімінг відповідей Laravel AI SDK - а отже, AI-чати й live-оновлення, запущені через Octane.

### Що саме не працювало

Laravel вміє стрімити відповідь двома способами: замикання може писати в вивід через `echo` і `flush()`, а може повертати генератор і віддавати чанки через `yield`. Поза Octane обидва варіанти працюють однаково. Під Octane `ResponseFactory::stream()` поводиться інакше: генераторне замикання він передає серверному клієнту Octane як є і чекає, що клієнт сам пройде генератор.

Як пояснює автор виправлення Паскаль Бальєт у [PR #1182](https://github.com/laravel/octane/pull/1182), робив це лише клієнт RoadRunner, і то тільки коли в замикання був оголошений тип повернення `Generator` або `string`. FrankenPHP і Swoole викликали `sendContent()` із Symfony, а той викликає замикання, але не ітерує генератор, який воно повертає. Код усередині генератора просто не виконувався, клієнт отримував порожню відповідь без жодної помилки.

Проблему описали в [issue laravel/framework#60406](https://github.com/laravel/framework/issues/60406): автор натрапив на неї саме з Laravel AI SDK на FrankenPHP. Варіант з `echo` і `flush()` при цьому працював, тож обхідний шлях існував. #1182 - уже четверта спроба виправлення після #1141, #1144 і #1163.

### Як це працює тепер

На FrankenPHP і Swoole Octane обгортає генераторне замикання: кожен чанк, що вийшов через `yield`, одразу виводиться і скидається клієнту, так само як Laravel стрімить поза Octane. RoadRunner тепер стрімить будь-яке замикання-генератор, а не лише з оголошеним типом `Generator`.

Ось маршрут з issue, який до оновлення віддавав порожнє тіло, а тепер працює:

```php
use Illuminate\Support\Facades\Route;

Route::get('/sse-yield', fn () => response()->stream(function (): iterable {
  yield "data: hello\n\n";
  yield "data: [DONE]\n\n";
}, headers: ['Content-Type' => 'text/event-stream']));
```

Замикання на `echo` і `flush()` поводяться як раніше. Автор перевірив зміну на реальних серверах FrankenPHP 1.13.4, Swoole 6.2.4 і RoadRunner 3.0.1.

### Посилення: стрім зупиняється разом із клієнтом

Щойно генератори почали реально виконуватися, стали досяжними крайні випадки, які раніше ніхто не бачив. Їх закрив наступний [PR #1183](https://github.com/laravel/octane/pull/1183):

- **Розрив з'єднання зупиняє стрім.** FrankenPHP повідомляє про нього через `connection_aborted()`, Swoole - через невдалий `write()` чергового чанка. Без цього нескінченний SSE-генератор працював і після того, як браузер закрили: на FrankenPHP тримав воркер безстроково, на Swoole - доки його не вбивав сторожовий таймер `max_execution_time` (за замовчуванням 30 секунд). Блоки `finally` генератора при цьому виконуються.
- **RoadRunner** при розриві з'єднання тепер тихо завершує стрім, а не кидає `StreamStoppedException`.
- **Swoole** закриває буфер виводу, коли стрімінгове замикання кидає виняток. Раніше буфер протікав у довгоживучий воркер.
- Octane тепер конфліктує з `spiral/roadrunner-http` нижче 3.4.0: у 3.3.0 немає `PSR7Worker::getHttpWorker()`, тож замикання з `: Generator` на ній і так падали.

Для SSE це важливіше, ніж здається. Воркерів в Octane небагато, і кожен «завислий» на закритій вкладці потік забирав один із них.

### Решта релізу

- **RoadRunner 3.x.** RoadRunner перейшов з календарних версій (2023.x-2025.x) на семантичні з v3.0.0. Composer вважав `3.0.0` меншою за `2023.1.0`, тож Octane конфліктував із новою лінійкою ([#1175](https://github.com/laravel/octane/pull/1175)), а перевірка бінарника вважала rr 3.x застарілим і без TTY щоразу перекачувала його при старті ([#1178](https://github.com/laravel/octane/pull/1178)). Обидва виправлено. Зважте: PHP-пакети RoadRunner 3 потребують PHP 8.2+, а автор #1178 прямо пише, що повної підтримки v3 ще немає - формат JSON-логів у v3 змінився, і `writeServerOutput()` його поки не враховує.
- **FrankenPHP 1.13.2 обов'язковий** ([#1176](https://github.com/laravel/octane/pull/1176)). Статичні бінарники FrankenPHP мають власну PHP, і в 1.13.0-1.13.1 вона вразлива до PHP2Shell - двох критичних RCE без автентифікації. Оновлення системної PHP тут не допомагає. Зі старішим бінарником `octane:start` попередить і запропонує завантажити свіжий реліз.
- **Бінарники FrankenPHP поза коренем проєкту більше не замінюються** ([#1177](https://github.com/laravel/octane/pull/1177)). Раніше Octane перейменовував, наприклад, `/usr/local/bin/frankenphp` і завантажував новий у корінь проєкту, після чого сервер не стартував. Тепер для такого бінарника Octane лише попереджає, що його треба оновити вручну.
- **Стандартні слухачі як singletons** ([#1174](https://github.com/laravel/octane/pull/1174)). 16 із приблизно 40 слухачів події `RequestReceived` контейнер збирав через рефлексію на кожен запит. Тепер кожен воркер будує їх один раз.

### Як оновитися

```bash
composer require laravel/octane:^2.23
```

Після оновлення перезапустіть воркери (`php artisan octane:reload` або рестарт сервісу), щоб вони підхопили новий код.

### Чи варто оновлюватися

Якщо ви стрімите відповіді під Octane на FrankenPHP чи Swoole - так, і варто одразу перевірити, чи не лишився в коді обхідний шлях з `echo` і `flush()`: тепер його можна прибрати. Те саме стосується тих, хто використовує стрімінг Laravel AI SDK.

На FrankenPHP оновлення потрібне і без стрімінгу, через вимогу 1.13.2: це закриває критичну вразливість. Майте на увазі, що під supervisor чи іншим демоном без TTY `octane:start` автоматично завантажить новий бінарник у корінь проєкту. Якщо FrankenPHP у вас у Docker-образі чи в `PATH`, оновіть його в образі самі - Octane лише попередить.

На RoadRunner перехід на 3.x тепер можливий, але не поспішайте, якщо ваш моніторинг читає логи Octane: формат логів v3 поки підтримується не повністю.
