From 823d2ccd8df7d141001c56946cfbaa1e3020b0a3 Mon Sep 17 00:00:00 2001 From: Vin Souza Date: Mon, 21 Jul 2025 11:05:39 +0100 Subject: [PATCH 1/5] chore: add stream responses --- composer.json | 2 +- src/Http/OctaneHandler.php | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 536045d..cc98c3e 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "require": { "php": "^8.0", "aws/aws-sdk-php": "^3.222", - "bref/bref": "^2.1.8", + "bref/bref": "2.4.9", "bref/laravel-health-check": "^1", "bref/monolog-bridge": "^1.0", "illuminate/container": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0", diff --git a/src/Http/OctaneHandler.php b/src/Http/OctaneHandler.php index d15af6e..8cd27fe 100644 --- a/src/Http/OctaneHandler.php +++ b/src/Http/OctaneHandler.php @@ -11,8 +11,11 @@ use Bref\Event\Http\HttpHandler; use Bref\Event\Http\HttpResponse; use Bref\Event\Http\HttpRequestEvent; - +use Bref\Event\Http\StreamedHttpResponse; +use Generator; +use ReflectionFunction; use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpFoundation\StreamedResponse; class OctaneHandler extends HttpHandler { @@ -29,7 +32,7 @@ public function __construct(?string $path = null) /** * {@inheritDoc} */ - public function handleRequest(HttpRequestEvent $event, Context $context): HttpResponse + public function handleRequest(HttpRequestEvent $event, Context $context): HttpResponse|StreamedHttpResponse { $request = Request::createFromBase( SymfonyRequestBridge::convertRequest($event, $context) @@ -45,6 +48,18 @@ public function handleRequest(HttpRequestEvent $event, Context $context): HttpRe $response->prepare($request); // https://github.com/laravel/framework/pull/43895 } + if ( + ($response instanceof StreamedResponse) && + ($responseCallback = $response->getCallback()) && + ((new ReflectionFunction($responseCallback))->getReturnType()?->getName() === Generator::class) + ) { + return new StreamedHttpResponse( + $responseCallback(), + $response->headers->all(), + $response->getStatusCode() + ); + } + $content = $response instanceof BinaryFileResponse ? $response->getFile()->getContent() : $response->getContent(); From 3d164e5b56cf8da45a571dab43e2ed8f13cda8d5 Mon Sep 17 00:00:00 2001 From: Vin Souza Date: Mon, 21 Jul 2025 11:06:31 +0100 Subject: [PATCH 2/5] chore: fix version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index cc98c3e..3254928 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "require": { "php": "^8.0", "aws/aws-sdk-php": "^3.222", - "bref/bref": "2.4.9", + "bref/bref": "^2.4.9", "bref/laravel-health-check": "^1", "bref/monolog-bridge": "^1.0", "illuminate/container": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0", From 5f4a952003a7645a21236fcd184049a24509fc99 Mon Sep 17 00:00:00 2001 From: Vin Souza Date: Mon, 21 Jul 2025 11:26:39 +0100 Subject: [PATCH 3/5] chore: use only http response --- src/Http/OctaneHandler.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Http/OctaneHandler.php b/src/Http/OctaneHandler.php index 8cd27fe..0df04d5 100644 --- a/src/Http/OctaneHandler.php +++ b/src/Http/OctaneHandler.php @@ -11,7 +11,6 @@ use Bref\Event\Http\HttpHandler; use Bref\Event\Http\HttpResponse; use Bref\Event\Http\HttpRequestEvent; -use Bref\Event\Http\StreamedHttpResponse; use Generator; use ReflectionFunction; use Symfony\Component\HttpFoundation\BinaryFileResponse; @@ -32,7 +31,7 @@ public function __construct(?string $path = null) /** * {@inheritDoc} */ - public function handleRequest(HttpRequestEvent $event, Context $context): HttpResponse|StreamedHttpResponse + public function handleRequest(HttpRequestEvent $event, Context $context): HttpResponse { $request = Request::createFromBase( SymfonyRequestBridge::convertRequest($event, $context) @@ -53,7 +52,7 @@ public function handleRequest(HttpRequestEvent $event, Context $context): HttpRe ($responseCallback = $response->getCallback()) && ((new ReflectionFunction($responseCallback))->getReturnType()?->getName() === Generator::class) ) { - return new StreamedHttpResponse( + return new HttpResponse( $responseCallback(), $response->headers->all(), $response->getStatusCode() From 0652e3cfb60577efbae659dff1a7c1b1667f1cbb Mon Sep 17 00:00:00 2001 From: Vin Souza Date: Mon, 21 Jul 2025 15:28:34 +0100 Subject: [PATCH 4/5] style: ignore ReflectionNamedType unknown getName --- src/Http/OctaneHandler.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Http/OctaneHandler.php b/src/Http/OctaneHandler.php index 0df04d5..620fbba 100644 --- a/src/Http/OctaneHandler.php +++ b/src/Http/OctaneHandler.php @@ -50,6 +50,7 @@ public function handleRequest(HttpRequestEvent $event, Context $context): HttpRe if ( ($response instanceof StreamedResponse) && ($responseCallback = $response->getCallback()) && + // @phpstan-ignore-next-line ((new ReflectionFunction($responseCallback))->getReturnType()?->getName() === Generator::class) ) { return new HttpResponse( From c7ee869463d41c385c20a00b9a388351a03c158f Mon Sep 17 00:00:00 2001 From: Vin Souza Date: Thu, 16 Oct 2025 16:13:04 +0100 Subject: [PATCH 5/5] chore: octane client prevent termination in the middle of yield with Fibers --- src/Octane/OctaneClient.php | 102 ++++++++++++++++++++++++++++++++++-- 1 file changed, 98 insertions(+), 4 deletions(-) diff --git a/src/Octane/OctaneClient.php b/src/Octane/OctaneClient.php index ae486a2..d94959c 100644 --- a/src/Octane/OctaneClient.php +++ b/src/Octane/OctaneClient.php @@ -2,6 +2,10 @@ namespace Bref\LaravelBridge\Octane; +use Bref\Bref; +use Bref\Event\Handler; +use Bref\Context\Context; +use Bref\Listener\BrefEventSubscriber; use Throwable; use Laravel\Octane\Worker; @@ -13,7 +17,7 @@ use Illuminate\Http\Request; use Illuminate\Foundation\Application; use Illuminate\Contracts\Debug\ExceptionHandler; - +use Psr\Http\Server\RequestHandlerInterface; use Symfony\Component\HttpFoundation\Response; class OctaneClient implements Client @@ -23,6 +27,9 @@ class OctaneClient implements Client */ private Worker $worker; + protected \Fiber|null $handleCurrentFiber = null; + protected bool $currentFiberHasResponded = false; + /** * The response of the last request that was processed. */ @@ -35,6 +42,24 @@ public function __construct(string $basePath, bool $persistDatabaseSession) )->boot()->onRequestHandled( static::manageDatabaseSessions($persistDatabaseSession) ); + + Bref::events()->subscribe( + new class ($this) extends BrefEventSubscriber { + public function __construct(protected OctaneClient $self) + { + } + + public function afterInvoke( + callable|Handler|RequestHandlerInterface $handler, + mixed $event, + Context $context, + mixed $result, + ?Throwable $error = null + ): void { // We listen to the afterInvoke method here so we can finish the fiber + $this->self->ensureExistingFiberIsTerminated(); + } + } + ); } /** @@ -45,9 +70,17 @@ public function __construct(string $basePath, bool $persistDatabaseSession) */ public function handle(Request $request): Response { + if (Bref::isRunningInStreamingMode()) { + if (Bref::doesStreamingSupportsFibers()) { + $this->ensureExistingFiberIsTerminated(); + + return $this->handleFiberableRequest($request); + } + } + $this->worker->application()->useStoragePath('/tmp/storage'); - $this->worker->handle($request, new RequestContext); + $this->worker->handle($request, new RequestContext()); $response = clone $this->response->response; $this->response = null; @@ -55,23 +88,72 @@ public function handle(Request $request): Response return $response; } + public function ensureExistingFiberIsTerminated() + { + if (($currentFiber = $this->handleCurrentFiber) instanceof \Fiber) { + if ($currentFiber->isStarted()) { + while (! $currentFiber->isTerminated()) { + $currentFiber->resume(); + } + } + + $this->handleCurrentFiber = null; + } + + $this->currentFiberHasResponded = false; + } + + protected function handleFiberableRequest(Request $request): Response + { + $this->handleCurrentFiber = new \Fiber( + function () use (&$request) { + $this->worker->application()->useStoragePath('/tmp/storage'); + + $this->worker->handle($request, new RequestContext()); + } + ); + + /** + * @var \Laravel\Octane\OctaneResponse $octaneResponse + */ + $octaneResponse = $this->handleCurrentFiber->start(); + + return $octaneResponse->response; + } + /** * {@inheritdoc} */ public function error(Throwable $exception, Application $app, Request $request, RequestContext $context): void { try { - $this->response = new OctaneResponse( + $response = new OctaneResponse( $app[ExceptionHandler::class]->render($request, $exception) ); } catch (Throwable $throwable) { fwrite(STDERR, $throwable->getMessage()); fwrite(STDERR, $exception->getMessage()); - $this->response = new OctaneResponse( + $response = new OctaneResponse( new Response('Internal Server Error', 500) ); } + + if (Bref::isRunningInStreamingMode()) { + if (Bref::doesStreamingSupportsFibers()) { + if (! $this->currentFiberHasResponded) { + $this->currentFiberHasResponded = true; + \Fiber::suspend($response); // If we are running in streaming mode and we support fiber, we suspend the response + } else { + fwrite(STDERR, "Request failed and already started sending: " . $exception->getMessage()); + } + return; + } else { + fwrite(STDERR, "Request running in Octane mode with streaming but no Fibers support, that can cause unwanted errors like Laravel's Container not booted"); + } + } + + $this->response = $response; } /** @@ -79,6 +161,18 @@ public function error(Throwable $exception, Application $app, Request $request, */ public function respond(RequestContext $context, OctaneResponse $response): void { + if (Bref::isRunningInStreamingMode()) { + if (Bref::doesStreamingSupportsFibers()) { + if (! $this->currentFiberHasResponded) { + $this->currentFiberHasResponded = true; + \Fiber::suspend($response); // If we are running in streaming mode and we support fiber, we suspend the response + } + return; + } else { + fwrite(STDERR, "Request running in Octane mode with streaming but no Fibers support, that can cause unwanted errors like Laravel's Container not booted"); + } + } + $this->response = $response; }