From ee3f121bb2d07e1a777a11aa430bbd55d2c35525 Mon Sep 17 00:00:00 2001 From: Artem Sityaev Date: Sun, 30 Jun 2024 16:06:16 +0700 Subject: [PATCH] Added API error separation by their HTTP codes --- src/Exceptions/APIError.php | 18 ++++++ src/Exceptions/APIStatusError.php | 30 ++++++++++ src/Exceptions/AuthenticationError.php | 8 +++ src/Exceptions/BadRequestError.php | 8 +++ src/Exceptions/ConflictError.php | 8 +++ src/Exceptions/ErrorException.php | 4 +- src/Exceptions/NotFoundError.php | 8 +++ src/Exceptions/PermissionDeniedError.php | 8 +++ src/Exceptions/RateLimitError.php | 8 +++ src/Exceptions/UnprocessableEntityError.php | 8 +++ src/Transporters/HttpTransporter.php | 64 ++++++++++++++++----- tests/Transporters/HttpTransporter.php | 59 ++++++++++++------- 12 files changed, 192 insertions(+), 39 deletions(-) create mode 100644 src/Exceptions/APIError.php create mode 100644 src/Exceptions/APIStatusError.php create mode 100644 src/Exceptions/AuthenticationError.php create mode 100644 src/Exceptions/BadRequestError.php create mode 100644 src/Exceptions/ConflictError.php create mode 100644 src/Exceptions/NotFoundError.php create mode 100644 src/Exceptions/PermissionDeniedError.php create mode 100644 src/Exceptions/RateLimitError.php create mode 100644 src/Exceptions/UnprocessableEntityError.php diff --git a/src/Exceptions/APIError.php b/src/Exceptions/APIError.php new file mode 100644 index 00000000..a6370fea --- /dev/null +++ b/src/Exceptions/APIError.php @@ -0,0 +1,18 @@ +request; + } +} diff --git a/src/Exceptions/APIStatusError.php b/src/Exceptions/APIStatusError.php new file mode 100644 index 00000000..4be4ba57 --- /dev/null +++ b/src/Exceptions/APIStatusError.php @@ -0,0 +1,30 @@ +statusCode = $response->getStatusCode(); + $this->requestId = $response->getHeader('x-request-id')[0] ?? null; + } + + public function getStatusCode(): int + { + return $this->statusCode; + } + + public function getRequestId(): ?string + { + return $this->requestId; + } +} diff --git a/src/Exceptions/AuthenticationError.php b/src/Exceptions/AuthenticationError.php new file mode 100644 index 00000000..3dface08 --- /dev/null +++ b/src/Exceptions/AuthenticationError.php @@ -0,0 +1,8 @@ +, type: ?string, code: string|int|null} $contents */ - public function __construct(private readonly array $contents) + public function __construct(protected readonly array $contents) { $message = ($contents['message'] ?: (string) $this->contents['code']) ?: 'Unknown error'; diff --git a/src/Exceptions/NotFoundError.php b/src/Exceptions/NotFoundError.php new file mode 100644 index 00000000..8a22a047 --- /dev/null +++ b/src/Exceptions/NotFoundError.php @@ -0,0 +1,8 @@ +toRequest($this->baseUri, $this->headers, $this->queryParams); - $response = $this->sendRequest(fn (): \Psr\Http\Message\ResponseInterface => $this->client->sendRequest($request)); + $response = $this->sendRequest( + fn (): \Psr\Http\Message\ResponseInterface => $this->client->sendRequest($request), + $request + ); $contents = $response->getBody()->getContents(); @@ -54,7 +65,7 @@ public function requestObject(Payload $payload): Response return Response::from($contents, $response->getHeaders()); } - $this->throwIfJsonError($response, $contents); + $this->throwIfJsonError($request, $response, $contents); try { /** @var array{error?: array{message: string, type: string, code: string}} $data */ @@ -73,11 +84,14 @@ public function requestContent(Payload $payload): string { $request = $payload->toRequest($this->baseUri, $this->headers, $this->queryParams); - $response = $this->sendRequest(fn (): \Psr\Http\Message\ResponseInterface => $this->client->sendRequest($request)); + $response = $this->sendRequest( + fn (): \Psr\Http\Message\ResponseInterface => $this->client->sendRequest($request), + $request + ); $contents = $response->getBody()->getContents(); - $this->throwIfJsonError($response, $contents); + $this->throwIfJsonError($request, $response, $contents); return $contents; } @@ -89,27 +103,38 @@ public function requestStream(Payload $payload): ResponseInterface { $request = $payload->toRequest($this->baseUri, $this->headers, $this->queryParams); - $response = $this->sendRequest(fn () => ($this->streamHandler)($request)); + $response = $this->sendRequest(fn () => ($this->streamHandler)($request), $request); - $this->throwIfJsonError($response, $response); + $this->throwIfJsonError($request, $response, $response); return $response; } - private function sendRequest(Closure $callable): ResponseInterface + private function sendRequest(Closure $callable, RequestInterface $request): ResponseInterface { try { return $callable(); } catch (ClientExceptionInterface $clientException) { if ($clientException instanceof ClientException) { - $this->throwIfJsonError($clientException->getResponse(), $clientException->getResponse()->getBody()->getContents()); + $this->throwIfJsonError($request, $clientException->getResponse(), $clientException->getResponse()->getBody()->getContents()); } throw new TransporterException($clientException); } } - private function throwIfJsonError(ResponseInterface $response, string|ResponseInterface $contents): void + /** + * @throws NotFoundError + * @throws RateLimitError + * @throws AuthenticationError + * @throws UnserializableResponse + * @throws ErrorException + * @throws BadRequestError + * @throws UnprocessableEntityError + * @throws ConflictError + * @throws PermissionDeniedError + */ + private function throwIfJsonError(RequestInterface $request, ResponseInterface $response, string|ResponseInterface $contents): void { if ($response->getStatusCode() < 400) { return; @@ -124,12 +149,21 @@ private function throwIfJsonError(ResponseInterface $response, string|ResponseIn } try { - /** @var array{error?: array{message: string|array, type: string, code: string}} $response */ - $response = json_decode($contents, true, flags: JSON_THROW_ON_ERROR); - - if (isset($response['error'])) { - throw new ErrorException($response['error']); - } + /** @var array{error: array{message: string|array, type: string, code: string}} $contentDecoded */ + $contentDecoded = json_decode($contents, true, flags: JSON_THROW_ON_ERROR); + + $error = $contentDecoded['error']; + + throw match ($response->getStatusCode()) { + 400 => new BadRequestError($request, $response, $error), + 401 => new AuthenticationError($request, $response, $error), + 403 => new PermissionDeniedError($request, $response, $error), + 404 => new NotFoundError($request, $response, $error), + 409 => new ConflictError($request, $response, $error), + 422 => new UnprocessableEntityError($request, $response, $error), + 429 => new RateLimitError($request, $response, $error), + default => new ErrorException($contentDecoded['error']), + }; } catch (JsonException $jsonException) { throw new UnserializableResponse($jsonException); } diff --git a/tests/Transporters/HttpTransporter.php b/tests/Transporters/HttpTransporter.php index 15a45b8f..fc91f5f3 100644 --- a/tests/Transporters/HttpTransporter.php +++ b/tests/Transporters/HttpTransporter.php @@ -4,7 +4,10 @@ use GuzzleHttp\Psr7\Request as Psr7Request; use GuzzleHttp\Psr7\Response; use OpenAI\Enums\Transporter\ContentType; +use OpenAI\Exceptions\AuthenticationError; use OpenAI\Exceptions\ErrorException; +use OpenAI\Exceptions\NotFoundError; +use OpenAI\Exceptions\RateLimitError; use OpenAI\Exceptions\TransporterException; use OpenAI\Exceptions\UnserializableResponse; use OpenAI\Transporters\HttpTransporter; @@ -101,11 +104,12 @@ ->andReturn($response); expect(fn () => $this->http->requestObject($payload)) - ->toThrow(function (ErrorException $e) { + ->toThrow(function (AuthenticationError $e) { expect($e->getMessage())->toBe('Incorrect API key provided: foo. You can find your API key at https://platform.openai.com.') ->and($e->getErrorMessage())->toBe('Incorrect API key provided: foo. You can find your API key at https://platform.openai.com.') ->and($e->getErrorCode())->toBe('invalid_api_key') - ->and($e->getErrorType())->toBe('invalid_request_error'); + ->and($e->getErrorType())->toBe('invalid_request_error') + ->and($e->getStatusCode())->toBe(401); }); }); @@ -153,11 +157,12 @@ ->andReturn($response); expect(fn () => $this->http->requestObject($payload)) - ->toThrow(function (ErrorException $e) { + ->toThrow(function (NotFoundError $e) { expect($e->getMessage())->toBe('The model `gpt-42` does not exist') ->and($e->getErrorMessage())->toBe('The model `gpt-42` does not exist') ->and($e->getErrorCode())->toBeNull() - ->and($e->getErrorType())->toBe('invalid_request_error'); + ->and($e->getErrorType())->toBe('invalid_request_error') + ->and($e->getStatusCode())->toBe(404); }); }); @@ -179,11 +184,12 @@ ->andReturn($response); expect(fn () => $this->http->requestObject($payload)) - ->toThrow(function (ErrorException $e) { + ->toThrow(function (NotFoundError $e) { expect($e->getMessage())->toBe('The model `gpt-42` does not exist') ->and($e->getErrorMessage())->toBe('The model `gpt-42` does not exist') ->and($e->getErrorCode())->toBe(123) - ->and($e->getErrorType())->toBe('invalid_request_error'); + ->and($e->getErrorType())->toBe('invalid_request_error') + ->and($e->getStatusCode())->toBe(404); }); }); @@ -205,11 +211,12 @@ ->andReturn($response); expect(fn () => $this->http->requestObject($payload)) - ->toThrow(function (ErrorException $e) { + ->toThrow(function (RateLimitError $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(); + ->and($e->getErrorType())->toBeNull() + ->and($e->getStatusCode())->toBe(429); }); }); @@ -234,11 +241,12 @@ ->andReturn($response); expect(fn () => $this->http->requestObject($payload)) - ->toThrow(function (ErrorException $e) { + ->toThrow(function (NotFoundError $e) { expect($e->getMessage())->toBe('Invalid schema for function \'get_current_weather\':'.PHP_EOL.'In context=(\'properties\', \'location\'), array schema missing items') ->and($e->getErrorMessage())->toBe('Invalid schema for function \'get_current_weather\':'.PHP_EOL.'In context=(\'properties\', \'location\'), array schema missing items') ->and($e->getErrorCode())->toBeNull() - ->and($e->getErrorType())->toBe('invalid_request_error'); + ->and($e->getErrorType())->toBe('invalid_request_error') + ->and($e->getStatusCode())->toBe(404); }); }); @@ -260,11 +268,12 @@ ->andReturn($response); expect(fn () => $this->http->requestObject($payload)) - ->toThrow(function (ErrorException $e) { + ->toThrow(function (NotFoundError $e) { expect($e->getMessage())->toBe('invalid_api_key') ->and($e->getErrorMessage())->toBe('invalid_api_key') ->and($e->getErrorCode())->toBe('invalid_api_key') - ->and($e->getErrorType())->toBe('invalid_request_error'); + ->and($e->getErrorType())->toBe('invalid_request_error') + ->and($e->getStatusCode())->toBe(404); }); }); @@ -286,11 +295,12 @@ ->andReturn($response); expect(fn () => $this->http->requestObject($payload)) - ->toThrow(function (ErrorException $e) { + ->toThrow(function (NotFoundError $e) { expect($e->getMessage())->toBe('123') ->and($e->getErrorMessage())->toBe('123') ->and($e->getErrorCode())->toBe(123) - ->and($e->getErrorType())->toBe('invalid_request_error'); + ->and($e->getErrorType())->toBe('invalid_request_error') + ->and($e->getStatusCode())->toBe(404); }); }); @@ -312,11 +322,12 @@ ->andReturn($response); expect(fn () => $this->http->requestObject($payload)) - ->toThrow(function (ErrorException $e) { + ->toThrow(function (NotFoundError $e) { expect($e->getMessage())->toBe('Unknown error') ->and($e->getErrorMessage())->toBe('Unknown error') ->and($e->getErrorCode())->toBeNull() - ->and($e->getErrorType())->toBe('invalid_request_error'); + ->and($e->getErrorType())->toBe('invalid_request_error') + ->and($e->getStatusCode())->toBe(404); }); }); @@ -362,9 +373,11 @@ ])) )); - expect(fn () => $this->http->requestObject($payload))->toThrow(function (ErrorException $e) { + expect(fn () => $this->http->requestObject($payload))->toThrow(function (AuthenticationError $e) { expect($e->getMessage()) - ->toBe('Incorrect API key provided: foo. You can find your API key at https://platform.openai.com.'); + ->toBe('Incorrect API key provided: foo. You can find your API key at https://platform.openai.com.') + ; + expect($e->getStatusCode())->toBe(401); }); }); @@ -471,11 +484,12 @@ ->andReturn($response); expect(fn () => $this->http->requestContent($payload)) - ->toThrow(function (ErrorException $e) { + ->toThrow(function (AuthenticationError $e) { expect($e->getMessage())->toBe('Incorrect API key provided: foo. You can find your API key at https://platform.openai.com.') ->and($e->getErrorMessage())->toBe('Incorrect API key provided: foo. You can find your API key at https://platform.openai.com.') ->and($e->getErrorCode())->toBe('invalid_api_key') - ->and($e->getErrorType())->toBe('invalid_request_error'); + ->and($e->getErrorType())->toBe('invalid_request_error') + ->and($e->getStatusCode())->toBe(401); }); }); @@ -523,10 +537,11 @@ ->andReturn($response); expect(fn () => $this->http->requestStream($payload)) - ->toThrow(function (ErrorException $e) { + ->toThrow(function (AuthenticationError $e) { expect($e->getMessage())->toBe('Incorrect API key provided: foo. You can find your API key at https://platform.openai.com.') ->and($e->getErrorMessage())->toBe('Incorrect API key provided: foo. You can find your API key at https://platform.openai.com.') ->and($e->getErrorCode())->toBe('invalid_api_key') - ->and($e->getErrorType())->toBe('invalid_request_error'); + ->and($e->getErrorType())->toBe('invalid_request_error') + ->and($e->getStatusCode())->toBe(401); }); });