Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/Exceptions/ServerException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace OpenAI\Exceptions;

use Exception;
use Psr\Http\Message\ResponseInterface;

final class ServerException extends Exception
{
public function __construct(public ResponseInterface $response)
{
parent::__construct("Server error (HTTP {$response->getStatusCode()}) occurred.");
}
}
20 changes: 19 additions & 1 deletion src/Transporters/HttpTransporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use OpenAI\Enums\Transporter\ContentType;
use OpenAI\Exceptions\ErrorException;
use OpenAI\Exceptions\RateLimitException;
use OpenAI\Exceptions\ServerException;
use OpenAI\Exceptions\TransporterException;
use OpenAI\Exceptions\UnserializableResponse;
use OpenAI\ValueObjects\Transporter\AdaptableResponse;
Expand Down Expand Up @@ -63,6 +64,7 @@ public function requestObject(Payload $payload): Response
$contents = (string) $response->getBody();

$this->throwIfRateLimit($response);
$this->throwIfServerError($response);
$this->throwIfJsonError($response, $contents);

try {
Expand All @@ -87,6 +89,7 @@ public function requestStringOrObject(Payload $payload): AdaptableResponse
$contents = (string) $response->getBody();

$this->throwIfRateLimit($response);
$this->throwIfServerError($response);
$this->throwIfJsonError($response, $contents);

if (str_contains($response->getHeaderLine('Content-Type'), ContentType::TEXT_PLAIN->value)) {
Expand Down Expand Up @@ -115,6 +118,7 @@ public function requestContent(Payload $payload): string
$contents = (string) $response->getBody();

$this->throwIfRateLimit($response);
$this->throwIfServerError($response);
$this->throwIfJsonError($response, $contents);

return $contents;
Expand All @@ -130,6 +134,7 @@ public function requestStream(Payload $payload): ResponseInterface
$response = $this->sendRequest(fn () => ($this->streamHandler)($request));

$this->throwIfRateLimit($response);
$this->throwIfServerError($response);
$this->throwIfJsonError($response, $response);

return $response;
Expand Down Expand Up @@ -157,6 +162,15 @@ private function throwIfRateLimit(ResponseInterface $response): void
throw new RateLimitException($response);
}

private function throwIfServerError(ResponseInterface $response): void
{
if ($response->getStatusCode() < 500) {
return;
}

throw new ServerException($response);
}

private function throwIfJsonError(ResponseInterface $response, string|ResponseInterface $contents): void
{
if ($response->getStatusCode() < 400) {
Expand All @@ -168,12 +182,16 @@ private function throwIfJsonError(ResponseInterface $response, string|ResponseIn
}

try {
/** @var array{error?: string|array{message: string|array<int, string>, type: string, code: string}} $data */
/** @var array{error?: string|array{message: string|array<int, string>, type: string, code: string}}|array<int, array{error?: string|array{message: string|array<int, string>, type: string, code: string}}> $data */
$data = json_decode($contents, true, flags: JSON_THROW_ON_ERROR);

if (isset($data['error'])) {
throw new ErrorException($data['error'], $response);
}

if (isset($data[0]['error'])) {
throw new ErrorException($data[0]['error'], $response);
}
} catch (JsonException $jsonException) {
// Due to some JSON coming back from OpenAI as text/plain, we need to avoid an early return from purely content-type checks.
if (! str_contains($response->getHeaderLine('Content-Type'), ContentType::JSON->value)) {
Expand Down
51 changes: 51 additions & 0 deletions tests/Transporters/HttpTransporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use OpenAI\Enums\Transporter\ContentType;
use OpenAI\Exceptions\ErrorException;
use OpenAI\Exceptions\RateLimitException;
use OpenAI\Exceptions\ServerException;
use OpenAI\Exceptions\TransporterException;
use OpenAI\Exceptions\UnserializableResponse;
use OpenAI\Responses\Models\ListResponse;
Expand Down Expand Up @@ -329,6 +330,34 @@
});
})->with('request methods');

test('error may be an nested array', function (string $requestMethod) {
$payload = Payload::create('completions', ['model' => 'gpt-4']);

$response = new Response(404, ['Content-Type' => 'application/json; charset=utf-8'], json_encode([
[
'error' => [
'message' => 'The engine is currently overloaded, please try again later',
'type' => 'invalid_request_error',
'param' => null,
'code' => null,
],
],
]));

$this->client
->shouldReceive('sendRequest')
->once()
->andReturn($response);

expect(fn () => $this->http->$requestMethod($payload))
->toThrow(function (ErrorException $e) {
expect($e->getMessage())->toBe('The engine is currently overloaded, please try again later')
->and($e->getErrorMessage())->toBe('The engine is currently overloaded, please try again later')
->and($e->getErrorCode())->toBeNull()
->and($e->getErrorType())->toBe('invalid_request_error');
});
})->with('request methods');

test('error message may be empty', function (string $requestMethod) {
$payload = Payload::create('completions', ['model' => 'gpt-4']);

Expand Down Expand Up @@ -580,6 +609,28 @@
});
});

test('request server errors', function () {
$payload = Payload::list('models');

$response = new Response(503, ['Content-Type' => 'application/json; charset=utf-8'], json_encode([
'error' => [
'code' => 503,
'message' => 'This model is currently experiencing high demand. Spikes in demand are usually temporary. Please try again later.',
'status' => 'UNAVAILABLE',
],
]));

$this->client
->shouldReceive('sendRequest')
->once()
->andReturn($response);

expect(fn () => $this->http->requestContent($payload))
->toThrow(function (ServerException $e) {
expect($e->getMessage())->toBe('Server error (HTTP 503) occurred.');
});
});

test('request stream', function () {
$payload = Payload::create('completions', []);

Expand Down