diff --git a/src/Connection/Internal/Http2ConnectionProcessor.php b/src/Connection/Internal/Http2ConnectionProcessor.php index 71fe070e..727426c9 100644 --- a/src/Connection/Internal/Http2ConnectionProcessor.php +++ b/src/Connection/Internal/Http2ConnectionProcessor.php @@ -1480,7 +1480,11 @@ private function shutdown(?HttpException $reason = null, ?int $lastId = null): v default => null, }; - $this->writeFrame(Http2Parser::GOAWAY, data: \pack('NN', 0, $code) . $message)->ignore(); + // Without active streams nothing else closes the socket, see releaseStream(). Closing it right away + // would discard the GOAWAY frame still waiting in the write queue. + $this->writeFrame(Http2Parser::GOAWAY, data: \pack('NN', 0, $code) . $message) + ->finally($this->socket->close(...)) + ->ignore(); foreach ($this->streams as $id => $_stream) { $reason ??= $this->makeDefaultShutdownReason(); diff --git a/test/Connection/Http2ConnectionTest.php b/test/Connection/Http2ConnectionTest.php index 3f4ff6cd..10a751de 100644 --- a/test/Connection/Http2ConnectionTest.php +++ b/test/Connection/Http2ConnectionTest.php @@ -591,6 +591,22 @@ public function testServerGoAwayFrame(): void } } + public function testCloseWithoutStreamsClosesSocket(): void + { + $this->connection->close(); + + $received = ''; + while (null !== $chunk = $this->server->read(new TimeoutCancellation(1))) { + $received .= $chunk; + } + + self::assertTrue($this->client->isClosed()); + self::assertStringEndsWith( + self::packFrame(\pack('NN', 0, Http2Parser::GRACEFUL_SHUTDOWN), Http2Parser::GOAWAY), + $received, + ); + } + public function testServerEarlyResponse(): void { $request = new Request('http://localhost/', 'POST', BufferedContent::fromString(\str_repeat('a', 2 ** 10)));