Skip to content

Commit 7b35cb6

Browse files
authored
Move logic outside exception (#933)
* Move logic outside exception * Explain why `defineResolveStatus` does not instanciate exception
1 parent decf924 commit 7b35cb6

17 files changed

+463
-142
lines changed

src/AbstractApi.php

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace AsyncAws\Core;
66

7+
use AsyncAws\Core\AwsError\AwsErrorFactoryInterface;
8+
use AsyncAws\Core\AwsError\ChainAwsErrorFactory;
79
use AsyncAws\Core\Credentials\CacheProvider;
810
use AsyncAws\Core\Credentials\ChainProvider;
911
use AsyncAws\Core\Credentials\CredentialProvider;
@@ -52,6 +54,11 @@ abstract class AbstractApi
5254
*/
5355
private $logger;
5456

57+
/**
58+
* @var AwsErrorFactoryInterface
59+
*/
60+
private $awsErrorFactory;
61+
5562
/**
5663
* @param Configuration|array $configuration
5764
*/
@@ -64,11 +71,17 @@ public function __construct($configuration = [], ?CredentialProvider $credential
6471
}
6572

6673
$this->logger = $logger ?? new NullLogger();
74+
$this->awsErrorFactory = $this->getAwsErrorFactory();
6775
if (!isset($httpClient)) {
6876
$httpClient = HttpClient::create();
6977
if (\class_exists(RetryableHttpClient::class)) {
7078
/** @psalm-suppress MissingDependency */
71-
$httpClient = new RetryableHttpClient($httpClient, new AwsRetryStrategy(), 3, $this->logger);
79+
$httpClient = new RetryableHttpClient(
80+
$httpClient,
81+
new AwsRetryStrategy(AwsRetryStrategy::DEFAULT_RETRY_STATUS_CODES, 1000, 2.0, 0, 0.1, $this->awsErrorFactory),
82+
3,
83+
$this->logger
84+
);
7285
}
7386
}
7487
$this->httpClient = $httpClient;
@@ -154,7 +167,7 @@ final protected function getResponse(Request $request, ?RequestContext $context
154167
]);
155168
}
156169

157-
return new Response($response, $this->httpClient, $this->logger, $debug);
170+
return new Response($response, $this->httpClient, $this->logger, $this->awsErrorFactory, $debug);
158171
}
159172

160173
/**
@@ -169,6 +182,11 @@ protected function getSignerFactories(): array
169182
];
170183
}
171184

185+
protected function getAwsErrorFactory(): AwsErrorFactoryInterface
186+
{
187+
return new ChainAwsErrorFactory();
188+
}
189+
172190
/**
173191
* Returns the AWS endpoint metadata for the given region.
174192
* When user did not provide a region, the client have to either return a global endpoint or fallback to
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace AsyncAws\Core\AwsError;
4+
5+
use Symfony\Contracts\HttpClient\ResponseInterface;
6+
7+
/**
8+
* @internal
9+
*/
10+
trait AwsErrorFactoryFromResponseTrait
11+
{
12+
public function createFromResponse(ResponseInterface $response): AwsError
13+
{
14+
$content = $response->getContent(false);
15+
$headers = $response->getHeaders(false);
16+
17+
return $this->createFromContent($content, $headers);
18+
}
19+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace AsyncAws\Core\AwsError;
4+
5+
use Symfony\Contracts\HttpClient\ResponseInterface;
6+
7+
/**
8+
* @internal
9+
*/
10+
interface AwsErrorFactoryInterface
11+
{
12+
public function createFromResponse(ResponseInterface $response): AwsError;
13+
14+
public function createFromContent(string $content, array $headers): AwsError;
15+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace AsyncAws\Core\AwsError;
4+
5+
use AsyncAws\Core\Exception\UnparsableResponse;
6+
7+
/**
8+
* @internal
9+
*/
10+
class ChainAwsErrorFactory implements AwsErrorFactoryInterface
11+
{
12+
use AwsErrorFactoryFromResponseTrait;
13+
14+
private $factories;
15+
16+
/**
17+
* @param AwsErrorFactoryInterface[]|null $factories
18+
*/
19+
public function __construct(array $factories = null)
20+
{
21+
$this->factories = $factories ?? [
22+
new JsonRestAwsErrorFactory(),
23+
new JsonRpcAwsErrorFactory(),
24+
new XmlAwsErrorFactory(),
25+
];
26+
}
27+
28+
public function createFromContent(string $content, array $headers): AwsError
29+
{
30+
$e = null;
31+
foreach ($this->factories as $factory) {
32+
try {
33+
return $factory->createFromContent($content, $headers);
34+
} catch (UnparsableResponse $e) {
35+
}
36+
}
37+
38+
throw new UnparsableResponse('Failed to parse AWS error: ' . $content, 0, $e);
39+
}
40+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace AsyncAws\Core\AwsError;
4+
5+
use AsyncAws\Core\Exception\UnexpectedValue;
6+
use AsyncAws\Core\Exception\UnparsableResponse;
7+
8+
/**
9+
* @internal
10+
*/
11+
class JsonRestAwsErrorFactory implements AwsErrorFactoryInterface
12+
{
13+
use AwsErrorFactoryFromResponseTrait;
14+
15+
public function createFromContent(string $content, array $headers): AwsError
16+
{
17+
try {
18+
$body = json_decode($content, true);
19+
20+
return self::parseJson($body, $headers);
21+
} catch (\Throwable $e) {
22+
throw new UnparsableResponse('Failed to parse AWS error: ' . $content, 0, $e);
23+
}
24+
}
25+
26+
private static function parseJson(array $body, array $headers): AwsError
27+
{
28+
$code = null;
29+
$type = $body['type'] ?? $body['Type'] ?? null;
30+
if ($type) {
31+
$type = \strtolower($type);
32+
}
33+
$message = $body['message'] ?? $body['Message'] ?? null;
34+
if (isset($headers['x-amzn-errortype'][0])) {
35+
$code = explode(':', $headers['x-amzn-errortype'][0], 2)[0];
36+
}
37+
38+
if (null !== $code) {
39+
return new AwsError($code, $message, $type, null);
40+
}
41+
42+
throw new UnexpectedValue('JSON does not contains AWS Error');
43+
}
44+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace AsyncAws\Core\AwsError;
4+
5+
use AsyncAws\Core\Exception\UnexpectedValue;
6+
use AsyncAws\Core\Exception\UnparsableResponse;
7+
8+
/**
9+
* @internal
10+
*/
11+
class JsonRpcAwsErrorFactory implements AwsErrorFactoryInterface
12+
{
13+
use AwsErrorFactoryFromResponseTrait;
14+
15+
public function createFromContent(string $content, array $headers): AwsError
16+
{
17+
try {
18+
$body = json_decode($content, true);
19+
20+
return self::parseJson($body, $headers);
21+
} catch (\Throwable $e) {
22+
throw new UnparsableResponse('Failed to parse AWS error: ' . $content, 0, $e);
23+
}
24+
}
25+
26+
private static function parseJson(array $body, array $headers): AwsError
27+
{
28+
$code = null;
29+
$message = $body['message'] ?? $body['Message'] ?? null;
30+
if (isset($body['__type'])) {
31+
$parts = explode('#', $body['__type'], 2);
32+
$code = $parts[1] ?? $parts[0];
33+
}
34+
35+
if (null !== $code || null !== $message) {
36+
return new AwsError($code, $message, null, null);
37+
}
38+
39+
throw new UnexpectedValue('JSON does not contains AWS Error');
40+
}
41+
}
Lines changed: 7 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,21 @@
55
use AsyncAws\Core\Exception\RuntimeException;
66
use AsyncAws\Core\Exception\UnexpectedValue;
77
use AsyncAws\Core\Exception\UnparsableResponse;
8-
use Symfony\Contracts\HttpClient\ResponseInterface;
98

109
/**
1110
* @internal
1211
*/
13-
class AwsErrorFactory
12+
class XmlAwsErrorFactory implements AwsErrorFactoryInterface
1413
{
15-
public static function createFromResponse(ResponseInterface $response): AwsError
16-
{
17-
$content = $response->getContent(false);
18-
$headers = $response->getHeaders(false);
19-
20-
return self::createFromContent($content, $headers);
21-
}
14+
use AwsErrorFactoryFromResponseTrait;
2215

23-
public static function createFromContent(string $content, array $headers): AwsError
16+
public function createFromContent(string $content, array $headers): AwsError
2417
{
2518
try {
26-
// Try json_decode it first, fallback to XML
27-
if ($body = json_decode($content, true)) {
28-
return self::parseJson($body, $headers);
29-
}
30-
31-
/** @phpstan-ignore-next-line */
19+
/**
20+
* @phpstan-ignore-next-line
21+
* @psalm-suppress InvalidArgument
22+
*/
3223
set_error_handler(static function ($errno, $errstr) {
3324
throw new RuntimeException($errstr, $errno);
3425
});
@@ -67,27 +58,4 @@ private static function parseXml(\SimpleXMLElement $xml): AwsError
6758

6859
throw new UnexpectedValue('XML does not contains AWS Error');
6960
}
70-
71-
private static function parseJson(array $body, array $headers): AwsError
72-
{
73-
$code = null;
74-
75-
$message = $body['message'] ?? $body['Message'] ?? null;
76-
if (isset($headers['x-amzn-errortype'][0])) {
77-
$code = explode(':', $headers['x-amzn-errortype'][0], 2)[0];
78-
}
79-
80-
$type = $body['type'] ?? $body['Type'] ?? null;
81-
if (isset($body['__type'])) {
82-
$parts = explode('#', $body['__type'], 2);
83-
$code = $parts[1] ?? $parts[0];
84-
$type = $parts[0];
85-
}
86-
87-
if (null !== $code || null !== $message) {
88-
return new AwsError($code, $message, $type, null);
89-
}
90-
91-
throw new UnexpectedValue('JSON does not contains AWS Error');
92-
}
9361
}

src/Exception/Http/HttpExceptionTrait.php

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
namespace AsyncAws\Core\Exception\Http;
44

55
use AsyncAws\Core\AwsError\AwsError;
6-
use AsyncAws\Core\AwsError\AwsErrorFactory;
7-
use AsyncAws\Core\Exception\UnparsableResponse;
86
use Symfony\Contracts\HttpClient\ResponseInterface;
97

108
/**
@@ -26,22 +24,16 @@ trait HttpExceptionTrait
2624
*/
2725
private $awsError;
2826

29-
public function __construct(ResponseInterface $response)
27+
public function __construct(ResponseInterface $response, ?AwsError $awsError)
3028
{
3129
$this->response = $response;
3230
/** @var int $code */
3331
$code = $response->getInfo('http_code');
3432
/** @var string $url */
3533
$url = $response->getInfo('url');
3634

37-
try {
38-
$this->awsError = AwsErrorFactory::createFromResponse($response);
39-
} catch (UnparsableResponse $e) {
40-
// Ignore parsing error
41-
}
42-
4335
$message = sprintf('HTTP %d returned for "%s".', $code, $url);
44-
if (null !== $this->awsError) {
36+
if (null !== $this->awsError = $awsError) {
4537
$message .= <<<TEXT
4638
4739

src/HttpClient/AwsRetryStrategy.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
namespace AsyncAws\Core\HttpClient;
44

5-
use AsyncAws\Core\AwsError\AwsErrorFactory;
5+
use AsyncAws\Core\AwsError\AwsErrorFactoryInterface;
6+
use AsyncAws\Core\AwsError\ChainAwsErrorFactory;
67
use AsyncAws\Core\Exception\UnparsableResponse;
78
use Symfony\Component\HttpClient\Response\AsyncContext;
89
use Symfony\Component\HttpClient\Retry\GenericRetryStrategy;
@@ -15,10 +16,13 @@ class AwsRetryStrategy extends GenericRetryStrategy
1516
{
1617
public const DEFAULT_RETRY_STATUS_CODES = [0, 423, 425, 429, 500, 502, 503, 504, 507, 510];
1718

19+
private $awsErrorFactory;
20+
1821
// Override Symfony default options for a better integration of AWS servers.
19-
public function __construct(array $statusCodes = self::DEFAULT_RETRY_STATUS_CODES, int $delayMs = 1000, float $multiplier = 2.0, int $maxDelayMs = 0, float $jitter = 0.1)
22+
public function __construct(array $statusCodes = self::DEFAULT_RETRY_STATUS_CODES, int $delayMs = 1000, float $multiplier = 2.0, int $maxDelayMs = 0, float $jitter = 0.1, AwsErrorFactoryInterface $awsErrorFactory = null)
2023
{
2124
parent::__construct($statusCodes, $delayMs, $multiplier, $maxDelayMs, $jitter);
25+
$this->awsErrorFactory = $awsErrorFactory ?? new ChainAwsErrorFactory();
2226
}
2327

2428
public function shouldRetry(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): ?bool
@@ -36,7 +40,7 @@ public function shouldRetry(AsyncContext $context, ?string $responseContent, ?Tr
3640
}
3741

3842
try {
39-
$error = AwsErrorFactory::createFromContent($responseContent, $context->getHeaders());
43+
$error = $this->awsErrorFactory->createFromContent($responseContent, $context->getHeaders());
4044
} catch (UnparsableResponse $e) {
4145
return false;
4246
}

0 commit comments

Comments
 (0)