diff --git a/src/Http/Client/LeverClient.php b/src/Http/Client/LeverClient.php index 83703d6..553727e 100644 --- a/src/Http/Client/LeverClient.php +++ b/src/Http/Client/LeverClient.php @@ -2,6 +2,7 @@ namespace Bluelightco\LeverPhp\Http\Client; +use Bluelightco\LeverPhp\Http\Exceptions\LeverApiException; use Bluelightco\LeverPhp\Http\Middleware\LeverRateStore; use Bluelightco\LeverPhp\Http\Middleware\QueryStringCleanerMiddleware; use Bluelightco\LeverPhp\Http\Responses\ApiResponse; @@ -14,7 +15,6 @@ use Illuminate\Support\LazyCollection; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; -use RuntimeException; use Spatie\GuzzleRateLimiterMiddleware\RateLimiterMiddleware; use Spatie\GuzzleRateLimiterMiddleware\Store; @@ -470,6 +470,14 @@ private function checkExpandOptions(array|string $expand): bool private function handleException(Exception $e, string $method, string $endpoint, array $options = []): void { + $statusCode = null; + $responseBody = null; + + if ($e instanceof ClientException && $e->getResponse()) { + $statusCode = $e->getResponse()->getStatusCode(); + $responseBody = (string) $e->getResponse()->getBody(); + } + Log::error("HTTP $method error: ".$e->getMessage(), [ 'message' => $e->getMessage(), 'package_context' => [ @@ -477,11 +485,16 @@ private function handleException(Exception $e, string $method, string $endpoint, 'method' => $method, 'endpoint' => $endpoint, 'options' => json_encode($options), - 'response' => ($e instanceof ClientException ? ($e->getResponse() ? $e->getResponse()->getBody()->getContents() : null) : null), + 'response' => $responseBody, ], 'exception' => $e, ]); - throw new RuntimeException("Error executing HTTP $method. Please check the logs for more details."); + throw new LeverApiException( + "Error executing HTTP $method. Please check the logs for more details.", + $statusCode, + $responseBody, + $e, + ); } } diff --git a/src/Http/Exceptions/LeverApiException.php b/src/Http/Exceptions/LeverApiException.php new file mode 100644 index 0000000..643a2f5 --- /dev/null +++ b/src/Http/Exceptions/LeverApiException.php @@ -0,0 +1,38 @@ +statusCode; + } + + public function getResponseBody(): ?string + { + return $this->responseBody; + } + + public function isClientError(): bool + { + return $this->statusCode !== null && $this->statusCode >= 400 && $this->statusCode < 500; + } + + public function isServerError(): bool + { + return $this->statusCode !== null && $this->statusCode >= 500 && $this->statusCode < 600; + } +} diff --git a/tests/OpportunitiesTest.php b/tests/OpportunitiesTest.php index daee78a..97ec773 100644 --- a/tests/OpportunitiesTest.php +++ b/tests/OpportunitiesTest.php @@ -2,6 +2,7 @@ namespace Bluelightco\LeverPhp\Tests; +use Bluelightco\LeverPhp\Http\Exceptions\LeverApiException; use GuzzleHttp\Psr7\Response; use Illuminate\Support\LazyCollection; use RuntimeException; @@ -129,6 +130,24 @@ public function fail_to_create_opportunity_when_no_perform_as_parameter_included ); } + /** @test */ + public function client_error_exposes_status_and_response_body() + { + $body = '{"code":"BadRequestError","message":"Input HTML is not valid."}'; + $this->mockHandler->append(new Response(400, [], $body)); + + try { + $this->lever->opportunities()->create(['name' => 'x']); + $this->fail('Expected LeverApiException was not thrown.'); + } catch (LeverApiException $e) { + $this->assertSame(400, $e->getStatusCode()); + $this->assertSame($body, $e->getResponseBody()); + $this->assertTrue($e->isClientError()); + $this->assertFalse($e->isServerError()); + $this->assertInstanceOf(\GuzzleHttp\Exception\ClientException::class, $e->getPrevious()); + } + } + /** @test */ public function fetching_offers() { diff --git a/tests/TestCase.php b/tests/TestCase.php index 5326e34..88f15c1 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -26,6 +26,8 @@ class TestCase extends \Orchestra\Testbench\TestCase protected function setUp(): void { + parent::setUp(); + $this->mockHandler = new MockHandler(); $this->container = [];