From cfc61c92ec13b15f6a7e08ab604e204968f08293 Mon Sep 17 00:00:00 2001 From: Connor Tumbleson Date: Thu, 21 Aug 2025 06:34:22 -0400 Subject: [PATCH] fix: handle 429 (rate limit) gracefully --- src/Exceptions/RateLimitException.php | 16 ++++++++++++++++ src/Transporters/HttpTransporter.php | 14 ++++++++++++++ tests/Transporters/HttpTransporter.php | 26 +++++++++++++++++++------- 3 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 src/Exceptions/RateLimitException.php diff --git a/src/Exceptions/RateLimitException.php b/src/Exceptions/RateLimitException.php new file mode 100644 index 00000000..b4521529 --- /dev/null +++ b/src/Exceptions/RateLimitException.php @@ -0,0 +1,16 @@ +getBody(); + $this->throwIfRateLimit($response); $this->throwIfJsonError($response, $contents); try { @@ -78,6 +80,7 @@ public function requestStringOrObject(Payload $payload): AdaptableResponse return AdaptableResponse::from($contents, $response->getHeaders()); } + $this->throwIfRateLimit($response); $this->throwIfJsonError($response, $contents); try { @@ -101,6 +104,7 @@ public function requestContent(Payload $payload): string $contents = (string) $response->getBody(); + $this->throwIfRateLimit($response); $this->throwIfJsonError($response, $contents); return $contents; @@ -115,6 +119,7 @@ public function requestStream(Payload $payload): ResponseInterface $response = $this->sendRequest(fn () => ($this->streamHandler)($request)); + $this->throwIfRateLimit($response); $this->throwIfJsonError($response, $response); return $response; @@ -133,6 +138,15 @@ private function sendRequest(Closure $callable): ResponseInterface } } + private function throwIfRateLimit(ResponseInterface $response): void + { + if ($response->getStatusCode() !== 429) { + return; + } + + throw new RateLimitException($response); + } + private function throwIfJsonError(ResponseInterface $response, string|ResponseInterface $contents): void { if ($response->getStatusCode() < 400) { diff --git a/tests/Transporters/HttpTransporter.php b/tests/Transporters/HttpTransporter.php index b8063f1c..688cf186 100644 --- a/tests/Transporters/HttpTransporter.php +++ b/tests/Transporters/HttpTransporter.php @@ -5,6 +5,7 @@ use GuzzleHttp\Psr7\Response; use OpenAI\Enums\Transporter\ContentType; use OpenAI\Exceptions\ErrorException; +use OpenAI\Exceptions\RateLimitException; use OpenAI\Exceptions\TransporterException; use OpenAI\Exceptions\UnserializableResponse; use OpenAI\Responses\Models\ListResponse; @@ -189,7 +190,7 @@ }); })->with('request methods'); -test('error type may be null', function (string $requestMethod) { +test('error type may be null on 429', function (string $requestMethod) { $payload = Payload::list('models'); $response = new Response(429, ['Content-Type' => 'application/json; charset=utf-8'], json_encode([ @@ -207,12 +208,23 @@ ->andReturn($response); expect(fn () => $this->http->$requestMethod($payload)) - ->toThrow(function (ErrorException $e) { - expect($e->getMessage())->toBe('You exceeded your current quota, please check') - ->and($e->getErrorMessage())->toBe('You exceeded your current quota, please check') - ->and($e->getErrorCode())->toBe('quota_exceeded') - ->and($e->getErrorType())->toBeNull(); - }); + ->toThrow(RateLimitException::class); +})->with('request methods'); + +test('429 may not follow OpenAI structure', function (string $requestMethod) { + $payload = Payload::list('models'); + + $response = new Response(429, ['Content-Type' => 'application/json; charset=utf-8'], json_encode([ + 'message' => 'Requests rate limit exceeded', + ])); + + $this->client + ->shouldReceive('sendRequest') + ->once() + ->andReturn($response); + + expect(fn () => $this->http->$requestMethod($payload)) + ->toThrow(RateLimitException::class); })->with('request methods'); test('error message may be an array', function (string $requestMethod) {