diff --git a/composer.json b/composer.json index 536045d..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.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..620fbba 100644 --- a/src/Http/OctaneHandler.php +++ b/src/Http/OctaneHandler.php @@ -11,8 +11,10 @@ use Bref\Event\Http\HttpHandler; use Bref\Event\Http\HttpResponse; use Bref\Event\Http\HttpRequestEvent; - +use Generator; +use ReflectionFunction; use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpFoundation\StreamedResponse; class OctaneHandler extends HttpHandler { @@ -45,6 +47,19 @@ 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()) && + // @phpstan-ignore-next-line + ((new ReflectionFunction($responseCallback))->getReturnType()?->getName() === Generator::class) + ) { + return new HttpResponse( + $responseCallback(), + $response->headers->all(), + $response->getStatusCode() + ); + } + $content = $response instanceof BinaryFileResponse ? $response->getFile()->getContent() : $response->getContent(); 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; }