From 1dc8b16d6a0e9c57df5ba8cbcef8f301a26ca182 Mon Sep 17 00:00:00 2001 From: Douglas Medeiros Date: Thu, 10 Sep 2026 17:21:35 -0300 Subject: [PATCH] fix(client): preserve MongoDB error labels from responses Co-authored-by: GPT-5.6 Sol Co-authored-by: Cursor --- src/Client.php | 3 ++- tests/ClientTest.php | 55 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index 9dcaacc..b659db3 100644 --- a/src/Client.php +++ b/src/Client.php @@ -1983,12 +1983,13 @@ private function parseResponse(string $response, int $responseLength): stdClass| if (\property_exists($result, 'errmsg')) { $code = (int)($result->code ?? 0); $name = (string)($result->codeName ?? 'MongoError'); + $errorLabels = isset($result->errorLabels) ? array_values((array) $result->errorLabels) : []; if (self::isPrimaryChange($code)) { $this->invalidate(preserveSessions: true); } - throw new Exception('E' . $code . ' ' . $name . ': ' . $result->errmsg, $code); + throw new Exception('E' . $code . ' ' . $name . ': ' . $result->errmsg, $code, null, $errorLabels); } if (!\property_exists($result, 'ok')) { diff --git a/tests/ClientTest.php b/tests/ClientTest.php index ddb1712..57ba6b4 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -684,6 +684,59 @@ public function testDecodedMongoCommandErrorDoesNotCloseTransport(): void $exception = $this->receiveException($client); $this->assertSame(11000, $exception->getCode()); + $this->assertSame('E11000 DuplicateKey: duplicate key', $exception->getMessage()); + $this->assertSame([], $exception->getErrorLabels()); + $this->assertSame([], $transport->closes); + $this->assertTrue($transport->open); + } + + public function testDecodedMongoCommandErrorPreservesServerLabels(): void + { + $transport = new SyncTransportDouble(); + $transport->receives = [[ + 'result' => $this->frame([ + 'ok' => 0.0, + 'errmsg' => 'host unreachable', + 'code' => 6, + 'codeName' => 'HostUnreachable', + 'errorLabels' => ['RetryableWriteError', 'TransientTransactionError'], + ]), + 'error' => 0, + ]]; + $client = $this->client($transport); + + $exception = $this->receiveException($client); + + $this->assertSame(['RetryableWriteError', 'TransientTransactionError'], $exception->getErrorLabels()); + $this->assertTrue($exception->isRetryableWrite()); + $this->assertTrue($exception->isTransientError()); + $this->assertFalse($exception->isUnsentError()); + $this->assertSame(6, $exception->getCode()); + $this->assertSame([], $transport->closes); + } + + public function testDecodedMongoCommandErrorTransientLabelDrivesTransientPredicate(): void + { + $transport = new SyncTransportDouble(); + $transport->receives = [[ + 'result' => $this->frame([ + 'ok' => 0.0, + 'errmsg' => 'command failed', + 'code' => 42, + 'codeName' => 'CommandFailed', + 'errorLabels' => ['TransientTransactionError'], + ]), + 'error' => 0, + ]]; + $client = $this->client($transport); + + $exception = $this->receiveException($client); + + $this->assertSame(['TransientTransactionError'], $exception->getErrorLabels()); + $this->assertTrue($exception->isTransientError()); + $this->assertFalse($exception->isRetryableWrite()); + $this->assertFalse($exception->isUnsentError()); + $this->assertSame(42, $exception->getCode()); $this->assertSame([], $transport->closes); $this->assertTrue($transport->open); } @@ -698,6 +751,7 @@ public function testPrimaryChangeErrorsCloseTransportAndPreserveSessions(): void 'errmsg' => 'primary changed', 'code' => $code, 'codeName' => 'PrimaryChanged', + 'errorLabels' => ['RetryableWriteError'], ]), 'error' => 0, ]]; @@ -709,6 +763,7 @@ public function testPrimaryChangeErrorsCloseTransportAndPreserveSessions(): void try { $this->assertSame($code, $exception->getCode()); + $this->assertSame(['RetryableWriteError'], $exception->getErrorLabels()); $this->assertSame([[true]], $transport->closes); $this->assertSame($sessions, $this->get($client, 'sessions')); $this->assertFalse($this->get($client, 'isConnected'));