From f3e9c907ba763f4f02b04224c341321adf2b6ef6 Mon Sep 17 00:00:00 2001 From: Matthew Keeler Date: Mon, 9 Nov 2026 12:36:28 -0500 Subject: [PATCH] fix: Give UnrecoverableHTTPStatusException a message and status code The exception reaches callers through EvaluationReason::getException(). Its message and code were empty, so the HTTP status was only visible through the internal status property. --- .../Impl/UnrecoverableHTTPStatusException.php | 2 ++ tests/LDClientTest.php | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/LaunchDarkly/Impl/UnrecoverableHTTPStatusException.php b/src/LaunchDarkly/Impl/UnrecoverableHTTPStatusException.php index 9f345dec1..f42f99e31 100644 --- a/src/LaunchDarkly/Impl/UnrecoverableHTTPStatusException.php +++ b/src/LaunchDarkly/Impl/UnrecoverableHTTPStatusException.php @@ -16,6 +16,8 @@ class UnrecoverableHTTPStatusException extends \Exception public function __construct(int $status) { + // Do not chain the Guzzle exception. Its request has the SDK key. + parent::__construct("HTTP error $status", $status); $this->status = $status; } } diff --git a/tests/LDClientTest.php b/tests/LDClientTest.php index 391a2f969..e29d47e06 100644 --- a/tests/LDClientTest.php +++ b/tests/LDClientTest.php @@ -2,10 +2,14 @@ namespace LaunchDarkly\Tests; +use GuzzleHttp\Handler\MockHandler; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Psr7\Response; use InvalidArgumentException; use LaunchDarkly\BigSegmentsEvaluationStatus; use LaunchDarkly\EvaluationDetail; use LaunchDarkly\EvaluationReason; +use LaunchDarkly\Impl\Integrations\GuzzleFeatureRequester; use LaunchDarkly\Impl\Model\FeatureFlag; use LaunchDarkly\Impl\UnrecoverableHTTPStatusException; use LaunchDarkly\LDClient; @@ -20,6 +24,7 @@ use LaunchDarkly\Types\BigSegmentsStoreMetadata; use LaunchDarkly\Types\BigSegmentsStoreStatus; use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; class LDClientTest extends \PHPUnit\Framework\TestCase { @@ -153,6 +158,30 @@ public function testUnrecoverableHTTPErrorStillTakesClientOffline(int $status): $this->assertNull($second->getReason()->getException()); } + /** @dataProvider unrecoverableHTTPStatuses */ + public function testVariationDetailExposesHTTPStatusInException(int $status): void + { + $requester = new GuzzleFeatureRequester('http://example.invalid', 'sdk-key', [ + 'logger' => new NullLogger(), + 'timeout' => 3, + 'connect_timeout' => 3, + ]); + $mock = new MockHandler([new Response($status)]); + $httpClient = new \GuzzleHttp\Client(['handler' => HandlerStack::create($mock)]); + (new \ReflectionProperty($requester, '_client'))->setValue($requester, $httpClient); + $client = $this->makeClient(['feature_requester' => $requester]); + + $detail = $client->variationDetail('feature', LDContext::create('userkey'), 'default'); + $exception = $detail->getReason()->getException(); + + $this->assertInstanceOf(UnrecoverableHTTPStatusException::class, $exception); + $this->assertSame("HTTP error $status", $exception->getMessage()); + $this->assertSame($status, $exception->getCode()); + $this->assertSame($status, $exception->status); + // The Guzzle exception holds the request and its Authorization header. + $this->assertNull($exception->getPrevious()); + } + public function testVariationReturnsDefaultIfFlagEvaluatesToNull() { $flag = $this->makeFlagThatEvaluatesToNull('feature');