From 12012c869132e02c11e295ce411faceb8c48a3ca Mon Sep 17 00:00:00 2001 From: Adam Jamiu Date: Sat, 24 Oct 2026 12:58:10 +0100 Subject: [PATCH] Fix client transport leak on reconnect Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/client/connecting.md | 2 ++ src/Client.php | 2 ++ tests/Unit/Client/ClientTest.php | 23 +++++++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/docs/client/connecting.md b/docs/client/connecting.md index 14f6ea7af..c4e5a15ac 100644 --- a/docs/client/connecting.md +++ b/docs/client/connecting.md @@ -214,6 +214,8 @@ speaks revision `2026-07-28`, the connection is ready. If it doesn't, the client `initialize` handshake and offers `2025-11-25`, or the revision set with `setFallbackProtocolVersion()`. A server process that exits fails the connection instead. +Calling `connect()` again on the same client closes its current transport before opening the new one. + With `setFallbackProtocolVersion(null)`, `connect()` fails instead. See [How the client settles on an era](../protocol-versions.md#how-the-client-settles-on-an-era). diff --git a/src/Client.php b/src/Client.php index f70fdeee5..993511de7 100644 --- a/src/Client.php +++ b/src/Client.php @@ -92,6 +92,8 @@ public static function builder(): Builder */ public function connect(TransportInterface $transport): void { + $this->disconnect(); + $this->transport = $transport; $this->protocol->connect($transport, $this->config); diff --git a/tests/Unit/Client/ClientTest.php b/tests/Unit/Client/ClientTest.php index 29d23ffeb..3c6da9a2a 100644 --- a/tests/Unit/Client/ClientTest.php +++ b/tests/Unit/Client/ClientTest.php @@ -21,6 +21,29 @@ final class ClientTest extends TestCase { + public function testReconnectClosesPreviousTransport(): void + { + $client = Client::builder()->build(); + $firstTransport = $this->createMock(TransportInterface::class); + $firstTransport->expects($this->once())->method('close'); + $firstTransport->method('setState')->willReturnCallback(static function (ClientStateInterface $state): void { + $state->setInitialized(true); + }); + + $secondTransport = $this->createMock(TransportInterface::class); + $secondTransport->expects($this->once())->method('close'); + $secondTransport->method('setState')->willReturnCallback(static function (ClientStateInterface $state): void { + $state->setInitialized(true); + }); + + $client->connect($firstTransport); + $client->connect($secondTransport); + + $this->assertTrue($client->isConnected()); + + $client->disconnect(); + } + public function testSendRootsListChangedSendsNotificationWhenCapabilityDeclared(): void { $sent = [];