From e2f6c3a4387cbe135b3db3c2fe82b015b47bea9c Mon Sep 17 00:00:00 2001 From: Johan Kromhout Date: Thu, 2 Apr 2026 14:21:19 +0200 Subject: [PATCH] Fix Guzzle regression: Error log was truncated / http exception not caught Guzzle 7 renamed the option that disables automatic exception throwing for 4xx/5xx responses from 'exceptions' to 'http_errors'. The old key was silently ignored, leaving Guzzle's httpErrors middleware active. This caused GuzzleHttp\Exception\ClientException to be thrown for any non-2xx response instead of returning the response object for the caller to inspect. In practice this meant that a 400 Bad Request from the attribute aggregation API (AA) was never caught by AttributeAggregator's HttpException handler and propagated as an uncaught CRITICAL-level exception. The exception message was truncated in the logs because Guzzle's BodySummarizer only includes the first 120 characters of the response body when building the ClientException message string. --- .../EngineBlock/Http/HttpClient.php | 4 +-- .../EngineBlock/Http/HttpClientTest.php | 34 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/OpenConext/EngineBlock/Http/HttpClient.php b/src/OpenConext/EngineBlock/Http/HttpClient.php index e5fb0c9cb3..9a76f7cbe6 100644 --- a/src/OpenConext/EngineBlock/Http/HttpClient.php +++ b/src/OpenConext/EngineBlock/Http/HttpClient.php @@ -53,7 +53,7 @@ public function read($path, array $parameters = [], array $headers = []) { $resource = ResourcePathFormatter::format($path, $parameters); $response = $this->httpClient->request('GET', $resource, [ - 'exceptions' => false, + 'http_errors' => false, 'headers' => $headers ]); $statusCode = $response->getStatusCode(); @@ -93,7 +93,7 @@ public function post($data, $path, $parameters = [], array $headers = [], bool $ { $resource = ResourcePathFormatter::format($path, $parameters); $response = $this->httpClient->request('POST', $resource, [ - 'exceptions' => false, + 'http_errors' => false, 'body' => $data, 'headers' => $headers, 'verify' => $verify, diff --git a/tests/unit/OpenConext/EngineBlock/Http/HttpClientTest.php b/tests/unit/OpenConext/EngineBlock/Http/HttpClientTest.php index 0e8337c528..2002b2c7bc 100644 --- a/tests/unit/OpenConext/EngineBlock/Http/HttpClientTest.php +++ b/tests/unit/OpenConext/EngineBlock/Http/HttpClientTest.php @@ -40,10 +40,12 @@ use GuzzleHttp\Client; use GuzzleHttp\Handler\MockHandler; +use GuzzleHttp\HandlerStack; use GuzzleHttp\Psr7\Response; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; use OpenConext\EngineBlock\Http\Exception\AccessDeniedException; use OpenConext\EngineBlock\Http\Exception\MalformedResponseException; +use OpenConext\EngineBlock\Http\Exception\UnreadableResourceException; use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; @@ -196,4 +198,36 @@ public function an_access_denied_exception_is_thrown_if_the_response_status_code $client->post('post-and-give-me/403', 'Post body'); } + + #[Group('EngineBlock')] + #[Group('Http')] + #[Test] + public function an_unreadable_resource_exception_is_thrown_if_the_response_status_code_is_400_when_reading() + { + $mockHandler = new MockHandler([ + new Response(400, [], '{"error":"Bad Request"}') + ]); + $guzzle = new Client(['handler' => HandlerStack::create($mockHandler)]); + $client = new HttpClient($guzzle); + + $this->expectException(UnreadableResourceException::class); + + $client->read('/some-resource'); + } + + #[Group('EngineBlock')] + #[Group('Http')] + #[Test] + public function an_unreadable_resource_exception_is_thrown_if_the_response_status_code_is_400_when_posting() + { + $mockHandler = new MockHandler([ + new Response(400, [], '{"error":"Bad Request"}') + ]); + $guzzle = new Client(['handler' => HandlerStack::create($mockHandler)]); + $client = new HttpClient($guzzle); + + $this->expectException(UnreadableResourceException::class); + + $client->post('{"id":1}', '/some-resource'); + } }