From d57af57d4d04405c6174964be5ca0d0afa8fc32e Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Mon, 9 Nov 2026 12:36:22 -0500 Subject: [PATCH] fix: Prevent allFlagsState from throwing when retrieving flags fails --- src/LaunchDarkly/LDClient.php | 3 +++ tests/LDClientTest.php | 41 +++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 7b33ae80..8cf0f494 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -594,6 +594,9 @@ public function allFlagsState(LDContext $context, array $options = []): FeatureF } catch (UnrecoverableHTTPStatusException $e) { $this->handleUnrecoverableError(); return new FeatureFlagsState(false); + } catch (\Throwable $e) { + Util::logExceptionAtErrorLevel($this->_logger, $e, "Unable to retrieve flags for allFlagsState"); + return new FeatureFlagsState(false); } if ($flags === null) { return new FeatureFlagsState(false); diff --git a/tests/LDClientTest.php b/tests/LDClientTest.php index 391a2f96..2b2b3b92 100644 --- a/tests/LDClientTest.php +++ b/tests/LDClientTest.php @@ -875,6 +875,47 @@ public function testAllFlagsClientSideVisiblityDoesNotAffectPrereqKeyList() $this->assertEquals($expectedState, $state->jsonSerialize()); } + public function allFlagsStateRequesterExceptions(): array + { + return [ + 'unexpected exception' => [new \RuntimeException('store unavailable')], + 'PHP error' => [new \Error('requester failed')], + ]; + } + + /** @dataProvider allFlagsStateRequesterExceptions */ + public function testAllFlagsStateReturnsInvalidStateWhenRequesterThrows(\Throwable $exception): void + { + $requester = $this->createMock(MockFeatureRequester::class); + $requester->method('getAllFeatures')->willThrowException($exception); + $logger = $this->createMock(LoggerInterface::class); + $logger->expects($this->once()) + ->method('error') + ->with($this->stringContains($exception->getMessage()), ['exception' => $exception]); + $client = $this->makeClient(['feature_requester' => $requester, 'logger' => $logger]); + + $state = $client->allFlagsState(LDContext::create('userkey')); + + $this->assertFalse($state->isValid()); + $this->assertEquals([], $state->toValuesMap()); + $this->assertFalse($client->isOffline()); + } + + /** @dataProvider unrecoverableHTTPStatuses */ + public function testAllFlagsStateUnrecoverableHTTPErrorTakesClientOffline(int $status): void + { + $requester = $this->createMock(MockFeatureRequester::class); + $requester->expects($this->once()) + ->method('getAllFeatures') + ->willThrowException(new UnrecoverableHTTPStatusException($status)); + $client = $this->makeClient(['feature_requester' => $requester]); + $context = LDContext::create('userkey'); + + $this->assertFalse($client->allFlagsState($context)->isValid()); + $this->assertTrue($client->isOffline()); + $this->assertFalse($client->allFlagsState($context)->isValid()); + } + public function testIdentifySendsEvent() { $ep = new MockEventProcessor();