Skip to content

Commit 4ca12b0

Browse files
committed
Enhance TransporterException with response details
TransporterException now includes the HTTP response, response body, and status code, improving error diagnostics. HttpTransporter passes these details when throwing TransporterException. Added tests to verify exception behavior with various error responses.
1 parent 05c406e commit 4ca12b0

3 files changed

Lines changed: 142 additions & 8 deletions

File tree

src/Exceptions/TransporterException.php

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,43 @@
66

77
use Exception;
88
use Psr\Http\Client\ClientExceptionInterface;
9+
use Psr\Http\Message\ResponseInterface;
910

1011
final class TransporterException extends Exception
1112
{
12-
/**
13-
* Creates a new Exception instance.
14-
*/
15-
public function __construct(ClientExceptionInterface $exception)
13+
private readonly ?ResponseInterface $response;
14+
15+
private readonly ?string $responseBody;
16+
17+
public function __construct(
18+
ClientExceptionInterface $exception,
19+
?ResponseInterface $response = null,
20+
?string $responseBody = null
21+
) {
22+
$this->response = $response;
23+
$this->responseBody = $responseBody;
24+
25+
$message = $exception->getMessage();
26+
27+
if ($responseBody !== null && $responseBody !== '') {
28+
$message .= ' | Response: ' . $responseBody;
29+
}
30+
31+
parent::__construct($message, $response?->getStatusCode() ?? 0, $exception);
32+
}
33+
34+
public function getResponse(): ?ResponseInterface
35+
{
36+
return $this->response;
37+
}
38+
39+
public function getResponseBody(): ?string
40+
{
41+
return $this->responseBody;
42+
}
43+
44+
public function getStatusCode(): int
1645
{
17-
parent::__construct($exception->getMessage(), 0, $exception);
46+
return $this->response?->getStatusCode() ?? 0;
1847
}
19-
}
48+
}

src/Transporters/HttpTransporter.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,16 @@ private function sendRequest(Closure $callable): ResponseInterface
140140
try {
141141
return $callable();
142142
} catch (ClientExceptionInterface $clientException) {
143+
$response = null;
144+
$responseBody = null;
145+
143146
if ($clientException instanceof ClientException) {
144-
$this->throwIfJsonError($clientException->getResponse(), (string) $clientException->getResponse()->getBody());
147+
$response = $clientException->getResponse();
148+
$responseBody = (string) $response->getBody();
149+
$this->throwIfJsonError($response, $responseBody);
145150
}
146151

147-
throw new TransporterException($clientException);
152+
throw new TransporterException($clientException, $response, $responseBody);
148153
}
149154
}
150155

tests/Transporters/HttpTransporter.php

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,3 +652,103 @@
652652

653653
$this->http->requestObject($payload);
654654
});
655+
656+
test('transporter exception includes response body for non-json client errors', function () {
657+
$payload = Payload::list('models');
658+
659+
$baseUri = BaseUri::from('api.openai.com');
660+
$headers = Headers::withAuthorization(ApiKey::from('foo'));
661+
$queryParams = QueryParams::create();
662+
663+
$responseBody = 'Bad Request: Invalid model parameter provided';
664+
665+
$this->client
666+
->shouldReceive('sendRequest')
667+
->once()
668+
->andThrow(new \GuzzleHttp\Exception\ClientException(
669+
message: 'Client error: 400 Bad Request',
670+
request: $payload->toRequest($baseUri, $headers, $queryParams),
671+
response: new Response(400, ['Content-Type' => 'text/plain'], $responseBody)
672+
));
673+
674+
expect(fn () => $this->http->requestObject($payload))->toThrow(function (TransporterException $e) use ($responseBody) {
675+
expect($e->getMessage())->toContain($responseBody)
676+
->and($e->getResponseBody())->toBe($responseBody)
677+
->and($e->getStatusCode())->toBe(400)
678+
->and($e->getResponse())->not->toBeNull();
679+
});
680+
});
681+
682+
test('transporter exception includes response body for html error responses', function () {
683+
$payload = Payload::list('models');
684+
685+
$baseUri = BaseUri::from('api.openai.com');
686+
$headers = Headers::withAuthorization(ApiKey::from('foo'));
687+
$queryParams = QueryParams::create();
688+
689+
$responseBody = '<html><body><h1>502 Bad Gateway</h1></body></html>';
690+
691+
$this->client
692+
->shouldReceive('sendRequest')
693+
->once()
694+
->andThrow(new \GuzzleHttp\Exception\ClientException(
695+
message: 'Server error: 502 Bad Gateway',
696+
request: $payload->toRequest($baseUri, $headers, $queryParams),
697+
response: new Response(502, ['Content-Type' => 'text/html'], $responseBody)
698+
));
699+
700+
expect(fn () => $this->http->requestObject($payload))->toThrow(function (TransporterException $e) use ($responseBody) {
701+
expect($e->getMessage())->toContain($responseBody)
702+
->and($e->getResponseBody())->toBe($responseBody)
703+
->and($e->getStatusCode())->toBe(502);
704+
});
705+
});
706+
707+
test('transporter exception includes openrouter style error response', function () {
708+
$payload = Payload::list('models');
709+
710+
$baseUri = BaseUri::from('api.openai.com');
711+
$headers = Headers::withAuthorization(ApiKey::from('foo'));
712+
$queryParams = QueryParams::create();
713+
714+
$responseBody = json_encode([
715+
'message' => 'Provider returned error',
716+
'code' => 400,
717+
'metadata' => ['provider' => 'anthropic'],
718+
]);
719+
720+
$this->client
721+
->shouldReceive('sendRequest')
722+
->once()
723+
->andThrow(new \GuzzleHttp\Exception\ClientException(
724+
message: 'Client error: 400 Bad Request',
725+
request: $payload->toRequest($baseUri, $headers, $queryParams),
726+
response: new Response(400, ['Content-Type' => 'application/json'], $responseBody)
727+
));
728+
729+
expect(fn () => $this->http->requestObject($payload))->toThrow(function (TransporterException $e) use ($responseBody) {
730+
expect($e->getMessage())->toContain('Provider returned error')
731+
->and($e->getResponseBody())->toBe($responseBody)
732+
->and($e->getStatusCode())->toBe(400);
733+
});
734+
});
735+
736+
test('transporter exception getters work without response', function () {
737+
$payload = Payload::list('models');
738+
739+
$baseUri = BaseUri::from('api.openai.com');
740+
$headers = Headers::withAuthorization(ApiKey::from('foo'));
741+
$queryParams = QueryParams::create();
742+
743+
$this->client
744+
->shouldReceive('sendRequest')
745+
->once()
746+
->andThrow(new ConnectException('Could not resolve host.', $payload->toRequest($baseUri, $headers, $queryParams)));
747+
748+
expect(fn () => $this->http->requestObject($payload))->toThrow(function (TransporterException $e) {
749+
expect($e->getMessage())->toBe('Could not resolve host.')
750+
->and($e->getResponseBody())->toBeNull()
751+
->and($e->getStatusCode())->toBe(0)
752+
->and($e->getResponse())->toBeNull();
753+
});
754+
});

0 commit comments

Comments
 (0)