diff --git a/src/Server/Transport/Http/StatelessResponder.php b/src/Server/Transport/Http/StatelessResponder.php index f752fb87..013cbd6b 100644 --- a/src/Server/Transport/Http/StatelessResponder.php +++ b/src/Server/Transport/Http/StatelessResponder.php @@ -80,6 +80,7 @@ private function sse(\Closure $frames): ResponseInterface echo null === $frame ? ": keep-alive\n\n" : 'data: '.json_encode($frame, \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES)."\n\n"; + @ob_flush(); flush(); } } catch (\Throwable $e) { diff --git a/tests/Integration/StatelessLifecycleTest.php b/tests/Integration/StatelessLifecycleTest.php index 1983d234..2f3cdd70 100644 --- a/tests/Integration/StatelessLifecycleTest.php +++ b/tests/Integration/StatelessLifecycleTest.php @@ -36,7 +36,9 @@ protected function setUp(): void { $this->port = 8600 + (getmypid() % 300); - $this->server = new Process(['php', '-S', \sprintf('127.0.0.1:%d', $this->port), self::SERVER]); + // Buffer output as php.ini-production and php.ini-development do, + // whatever php.ini is loaded here, so an unflushed stream shows up. + $this->server = new Process(['php', '-d', 'output_buffering=4096', '-S', \sprintf('127.0.0.1:%d', $this->port), self::SERVER]); $this->server->start(); $deadline = microtime(true) + 5; @@ -186,6 +188,37 @@ public function testLoggingIsSilentWithoutALevel(): void $this->assertContains('notifications/progress', $methods); } + #[TestDox('a listen stream is acknowledged at once, not when it closes')] + public function testListenIsAcknowledgedAtOnce(): void + { + $context = stream_context_create(['http' => [ + 'method' => 'POST', + 'header' => implode("\r\n", $this->headers('subscriptions/listen', null, null)), + 'content' => $this->body('subscriptions/listen', ['notifications' => ['toolsListChanged' => true]], null), + 'ignore_errors' => true, + // Well inside the example's 20 s lifetime: a frame held back in an + // output buffer would only arrive when the stream closes. + 'timeout' => 5, + ]]); + + $handle = fopen($this->url(), 'r', false, $context); + $this->assertIsResource($handle); + + $first = null; + while (false !== $line = fgets($handle)) { + $line = trim($line); + + if (str_starts_with($line, 'data: ')) { + $first = json_decode(substr($line, 6), true, flags: \JSON_THROW_ON_ERROR); + break; + } + } + + fclose($handle); + + $this->assertSame('notifications/subscriptions/acknowledged', $first['method'] ?? null); + } + /** * @param array $params * @param array $capabilities