Skip to content

Commit 3c72908

Browse files
committed
refactor: enhance aws client exception messages with aws error details
1 parent e0f9c66 commit 3c72908

File tree

6 files changed

+65
-80
lines changed

6 files changed

+65
-80
lines changed

src/CloudProvider/Aws/AbstractClient.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public function __construct(ClientInterface $client, string $key, string $region
6464
$this->key = $key;
6565
$this->region = $region;
6666
$this->secret = $secret;
67+
// TODO: Maybe this should be a constructor argument?
6768
$this->securityToken = getenv('AWS_SESSION_TOKEN') ?: '';
6869
}
6970

@@ -80,6 +81,40 @@ protected function createBaseHeaders(?string $body): array
8081
]);
8182
}
8283

84+
/**
85+
* Create an enhanced exception message with AWS error details.
86+
*/
87+
protected function createExceptionMessage(string $message, array $response): string
88+
{
89+
$message .= sprintf(' (HTTP %s)', $this->parseResponseStatusCode($response));
90+
91+
if (empty($response['body']) || !is_string($response['body'])) {
92+
return $message;
93+
}
94+
95+
$awsError = $this->parseAwsError($response['body']);
96+
97+
if (empty($awsError['code'])) {
98+
return $message;
99+
}
100+
101+
$message .= sprintf(' - AWS Error: %s', $awsError['code']);
102+
103+
if (empty($awsError['message'])) {
104+
return $message;
105+
}
106+
107+
$message .= sprintf(' (%s)', $awsError['message']);
108+
109+
if (empty($awsError['request_id'])) {
110+
return $message;
111+
}
112+
113+
$message .= sprintf(' [Request ID: %s]', $awsError['request_id']);
114+
115+
return $message;
116+
}
117+
83118
/**
84119
* Creates a presigned request for the given key and method.
85120
*
@@ -119,6 +154,20 @@ protected function getHostname(): string
119154
return "{$this->getEndpointName()}.{$this->region}.amazonaws.com";
120155
}
121156

157+
/**
158+
* Parse AWS error details from the response body.
159+
*/
160+
protected function parseAwsError(string $body): ?array
161+
{
162+
$xml = simplexml_load_string($body);
163+
164+
return $xml instanceof \SimpleXMLElement ? [
165+
'code' => (string) ($xml->Error->Code ?? $xml->Code ?? ''),
166+
'message' => (string) ($xml->Error->Message ?? $xml->Message ?? ''),
167+
'request_id' => (string) ($xml->RequestId ?? ''),
168+
] : null;
169+
}
170+
122171
/**
123172
* Parse the status code from the given response.
124173
*/

src/CloudProvider/Aws/CloudFrontClient.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,8 @@ private function createInvalidation($paths)
132132

133133
$response = $this->request('post', "/2020-05-31/distribution/{$this->distributionId}/invalidation", $this->generateInvalidationPayload($paths));
134134

135-
// TODO: Need to get the error message
136135
if (201 !== $this->parseResponseStatusCode($response)) {
137-
throw new \RuntimeException('Invalidation request failed');
136+
throw new \RuntimeException($this->createExceptionMessage('Invalidation request failed', $response));
138137
}
139138
}
140139

src/CloudProvider/Aws/DynamoDbClient.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public function batchGetItem(array $arguments): array
2626
$response = $this->perform('BatchGetItem', $arguments);
2727

2828
if (200 !== $this->parseResponseStatusCode($response)) {
29-
throw new \RuntimeException('Unable to get cache items');
29+
throw new \RuntimeException($this->createExceptionMessage('Unable to get cache items', $response));
3030
}
3131

3232
$items = json_decode($response['body'], true);
@@ -46,7 +46,7 @@ public function deleteItem(array $arguments)
4646
$response = $this->perform('DeleteItem', $arguments);
4747

4848
if (200 !== $this->parseResponseStatusCode($response)) {
49-
throw new \RuntimeException('Unable to delete cache item');
49+
throw new \RuntimeException($this->createExceptionMessage('Unable to delete cache item', $response));
5050
}
5151
}
5252

@@ -58,7 +58,7 @@ public function getItem(array $arguments)
5858
$response = $this->perform('GetItem', $arguments);
5959

6060
if (200 !== $this->parseResponseStatusCode($response)) {
61-
throw new \RuntimeException('Unable to delete cache item');
61+
throw new \RuntimeException($this->createExceptionMessage('Unable to get cache item', $response));
6262
}
6363

6464
$item = json_decode($response['body'], true);
@@ -78,7 +78,7 @@ public function putItem(array $arguments)
7878
$response = $this->perform('PutItem', $arguments);
7979

8080
if (200 !== $this->parseResponseStatusCode($response)) {
81-
throw new \RuntimeException('Unable to save cache item');
81+
throw new \RuntimeException($this->createExceptionMessage('Unable to save cache item', $response));
8282
}
8383
}
8484

src/CloudProvider/Aws/S3Client.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public function copyObject(string $sourceKey, string $targetKey, string $acl = '
4949
]);
5050

5151
if (200 !== $this->parseResponseStatusCode($response)) {
52-
throw new \RuntimeException(sprintf('Could not copy object "%s"', $sourceKey));
52+
throw new \RuntimeException($this->createExceptionMessage(sprintf('Could not copy object "%s"', $sourceKey), $response));
5353
}
5454
}
5555

@@ -71,7 +71,7 @@ public function deleteObject(string $key)
7171
$response = $this->request('delete', $key);
7272

7373
if (204 !== $this->parseResponseStatusCode($response)) {
74-
throw new \RuntimeException(sprintf('Unable to delete object "%s"', $key));
74+
throw new \RuntimeException($this->createExceptionMessage(sprintf('Unable to delete object "%s"', $key), $response));
7575
}
7676
}
7777

@@ -83,7 +83,7 @@ public function getObject(string $key): string
8383
$response = $this->request('get', $key);
8484

8585
if (200 !== $this->parseResponseStatusCode($response)) {
86-
throw new \RuntimeException(sprintf('Object "%s" not found', $key));
86+
throw new \RuntimeException($this->createExceptionMessage(sprintf('Object "%s" not found', $key), $response));
8787
}
8888

8989
return $response['body'] ?? '';
@@ -97,7 +97,7 @@ public function getObjectDetails(string $key): array
9797
$response = $this->request('head', $key);
9898

9999
if (200 !== $this->parseResponseStatusCode($response)) {
100-
throw new \RuntimeException(sprintf('Object "%s" not found', $key));
100+
throw new \RuntimeException($this->createExceptionMessage(sprintf('Object "%s" not found', $key), $response));
101101
}
102102

103103
$details = [];
@@ -144,7 +144,7 @@ public function getObjects(string $prefix, int $limit = 0): array
144144
$response = $this->request('get', '/?'.http_build_query($parameters));
145145

146146
if (200 !== $this->parseResponseStatusCode($response)) {
147-
throw new \RuntimeException(sprintf('Unable to list objects with prefix "%s"', $prefix));
147+
throw new \RuntimeException($this->createExceptionMessage(sprintf('Unable to list objects with prefix "%s"', $prefix), $response));
148148
} elseif (empty($response['body'])) {
149149
throw new \RuntimeException('No content returned from S3 API');
150150
}
@@ -189,7 +189,7 @@ public function putObject(string $key, string $object, string $acl = 'public-rea
189189
$response = $this->request('put', $key, $object, $headers);
190190

191191
if (200 !== $this->parseResponseStatusCode($response)) {
192-
throw new \RuntimeException(sprintf('Unable to save object "%s"', $key));
192+
throw new \RuntimeException($this->createExceptionMessage(sprintf('Unable to save object "%s"', $key), $response));
193193
}
194194
}
195195

src/CloudProvider/Aws/SesClient.php

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public function canSendEmails(): bool
2929
$response = $this->request('get', '/v2/email/account');
3030

3131
if (200 !== $this->parseResponseStatusCode($response)) {
32-
throw new \RuntimeException('Unable to get SES account details');
32+
throw new \RuntimeException($this->createExceptionMessage('Unable to get SES account details', $response));
3333
}
3434

3535
$response = json_decode($response['body'], true);
@@ -52,12 +52,9 @@ public function sendEmail(Email $email)
5252
'Action' => 'SendRawEmail',
5353
'RawMessage.Data' => base64_encode($email->toString()),
5454
]));
55-
$statusCode = $this->parseResponseStatusCode($response);
5655

57-
if (400 === $statusCode) {
58-
throw new \RuntimeException(sprintf('SES API request failed: %s', $this->getErrorMessage($response['body'] ?? '')));
59-
} elseif (200 !== $this->parseResponseStatusCode($response)) {
60-
throw new \RuntimeException(sprintf('SES API request failed with status code %d', $statusCode));
56+
if (200 !== $this->parseResponseStatusCode($response)) {
57+
throw new \RuntimeException($this->createExceptionMessage('SES API request failed', $response));
6158
}
6259
}
6360

@@ -76,18 +73,4 @@ protected function getService(): string
7673
{
7774
return 'ses';
7875
}
79-
80-
/**
81-
* Get the SES error message.
82-
*/
83-
private function getErrorMessage($body): string
84-
{
85-
$body = simplexml_load_string($body);
86-
87-
if (!$body instanceof \SimpleXMLElement) {
88-
return '[unable to parse error message]';
89-
}
90-
91-
return (string) $body->Error->Message;
92-
}
9376
}

tests/Unit/CloudProvider/Aws/SesClientTest.php

Lines changed: 2 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public function testSendEmail()
7474
public function testSendEmailWithSesError()
7575
{
7676
$this->expectException(\RuntimeException::class);
77-
$this->expectExceptionMessage('SES API request failed: Email address is not verified. The following identities failed the check in region US-EAST-1: WordPress <[email protected]>');
77+
$this->expectExceptionMessage('SES API request failed (HTTP 400) - AWS Error: MessageRejected (Email address is not verified. The following identities failed the check in region US-EAST-1: WordPress <[email protected]>) [Request ID: da8072bc-3550-4d98-81f5-28169cc53c7b]');
7878

7979
$email = $this->getEmailMock();
8080
$email->expects($this->once())
@@ -121,7 +121,7 @@ public function testSendEmailWithSesError()
121121
public function testSendEmailWithSesErrorAndNoBody()
122122
{
123123
$this->expectException(\RuntimeException::class);
124-
$this->expectExceptionMessage('SES API request failed: [unable to parse error message]');
124+
$this->expectExceptionMessage('SES API request failed (HTTP 400)');
125125

126126
$email = $this->getEmailMock();
127127
$email->expects($this->once())
@@ -163,50 +163,4 @@ public function testSendEmailWithSesErrorAndNoBody()
163163

164164
(new SesClient($http, 'aws-key', 'us-east-1', 'aws-secret'))->sendEmail($email);
165165
}
166-
167-
public function testSendEmailWithWrongStatusCode()
168-
{
169-
$this->expectException(\RuntimeException::class);
170-
$this->expectExceptionMessage('SES API request failed with status code 404');
171-
172-
$email = $this->getEmailMock();
173-
$email->expects($this->once())
174-
->method('toString')
175-
->willReturn('email');
176-
177-
$http = $this->getHttpClientMock();
178-
$http->expects($this->once())
179-
->method('request')
180-
->with(
181-
$this->identicalTo('https://email.us-east-1.amazonaws.com/'),
182-
$this->identicalTo([
183-
'headers' => [
184-
'host' => 'email.us-east-1.amazonaws.com',
185-
'x-amz-content-sha256' => '93c8df40dd7aabcd009385e2496d874342612b116f80638899066a8f6a2e72e6',
186-
'x-amz-date' => '20200515T181004Z',
187-
'authorization' => 'AWS4-HMAC-SHA256 Credential=aws-key/20200515/us-east-1/ses/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=105ad9051f2aaeb3471e626dfd368d2bccf87ea0626ace1ea0fbf459864eb62f',
188-
'content-length' => 46,
189-
],
190-
'method' => 'POST',
191-
'timeout' => 300,
192-
'body' => 'Action=SendRawEmail&RawMessage.Data=ZW1haWw%3D',
193-
])
194-
)
195-
->willReturn([
196-
'response' => ['code' => 404],
197-
]);
198-
199-
$gmdate = $this->getFunctionMock($this->getNamespace(SesClient::class), 'gmdate');
200-
$gmdate->expects($this->exactly(5))
201-
->withConsecutive(
202-
[$this->identicalTo('Ymd\THis\Z')],
203-
[$this->identicalTo('Ymd')],
204-
[$this->identicalTo('Ymd\THis\Z')],
205-
[$this->identicalTo('Ymd')],
206-
[$this->identicalTo('Ymd')]
207-
)
208-
->willReturnOnConsecutiveCalls('20200515T181004Z', '20200515', '20200515T181004Z', '20200515', '20200515');
209-
210-
(new SesClient($http, 'aws-key', 'us-east-1', 'aws-secret'))->sendEmail($email);
211-
}
212166
}

0 commit comments

Comments
 (0)