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
120 changes: 106 additions & 14 deletions aws/aws-sdk-php/src/Api/ErrorParser/JsonParserTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,41 +12,133 @@ trait JsonParserTrait
{
use PayloadParserTrait;

private function genericHandler(ResponseInterface $response)
private function genericHandler(ResponseInterface $response): array
{
$code = (string) $response->getStatusCode();
$error_code = null;
$error_type = null;

// Parse error code and type for query compatible services
if ($this->api
&& !is_null($this->api->getMetadata('awsQueryCompatible'))
&& $response->getHeaderLine('x-amzn-query-error')
&& $response->hasHeader('x-amzn-query-error')
) {
$queryError = $response->getHeaderLine('x-amzn-query-error');
$parts = explode(';', $queryError);
if (isset($parts) && count($parts) == 2 && $parts[0] && $parts[1]) {
$error_code = $parts[0];
$error_type = $parts[1];
$awsQueryError = $this->parseAwsQueryCompatibleHeader($response);
if ($awsQueryError) {
$error_code = $awsQueryError['code'];
$error_type = $awsQueryError['type'];
}
}

// Parse error code from X-Amzn-Errortype header
if (!$error_code && $response->hasHeader('X-Amzn-Errortype')) {
$error_code = $this->extractErrorCode(
$response->getHeaderLine('X-Amzn-Errortype')
);
}

$parsedBody = null;
$body = $response->getBody();
if (!$body->isSeekable() || $body->getSize()) {
$parsedBody = $this->parseJson((string) $body, $response);
}

// Parse error code from response body
if (!$error_code && $parsedBody) {
$error_code = $this->parseErrorFromBody($parsedBody);
}

if (!isset($error_type)) {
$error_type = $code[0] == '4' ? 'client' : 'server';
}

return [
'request_id' => (string) $response->getHeaderLine('x-amzn-requestid'),
'code' => isset($error_code) ? $error_code : null,
'request_id' => $response->getHeaderLine('x-amzn-requestid'),
'code' => $error_code ?? null,
'message' => null,
'type' => $error_type,
'parsed' => $this->parseJson($response->getBody(), $response)
'parsed' => $parsedBody
];
}

/**
* Parse AWS Query Compatible error from header
*
* @param ResponseInterface $response
* @return array|null Returns ['code' => string, 'type' => string] or null
*/
private function parseAwsQueryCompatibleHeader(ResponseInterface $response): ?array
{
$queryError = $response->getHeaderLine('x-amzn-query-error');
$parts = explode(';', $queryError);

if (count($parts) === 2 && $parts[0] && $parts[1]) {
return [
'code' => $parts[0],
'type' => $parts[1]
];
}

return null;
}

/**
* Parse error code from response body
*
* @param array|null $parsedBody
* @return string|null
*/
private function parseErrorFromBody(?array $parsedBody): ?string
{
if (!$parsedBody
|| (!isset($parsedBody['code']) && !isset($parsedBody['__type']))
) {
return null;
}

$error_code = $parsedBody['code'] ?? $parsedBody['__type'];
return $this->extractErrorCode($error_code);
}

/**
* Extract error code from raw error string containing # and/or : delimiters
*
* @param string $rawErrorCode
* @return string
*/
private function extractErrorCode(string $rawErrorCode): string
{
// Handle format with both # and uri (e.g., "namespace#http://foo-bar")
if (str_contains($rawErrorCode, ':') && str_contains($rawErrorCode, '#')) {
$start = strpos($rawErrorCode, '#') + 1;
$end = strpos($rawErrorCode, ':', $start);
return substr($rawErrorCode, $start, $end - $start);
}

// Handle format with uri only : (e.g., "ErrorCode:http://foo-bar.com/baz")
if (str_contains($rawErrorCode, ':')) {
return substr($rawErrorCode, 0, strpos($rawErrorCode, ':'));
}

// Handle format with only # (e.g., "namespace#ErrorCode")
if (str_contains($rawErrorCode, '#')) {
return substr($rawErrorCode, strpos($rawErrorCode, '#') + 1);
}

return $rawErrorCode;
}

protected function payload(
ResponseInterface $response,
StructureShape $member
) {
$jsonBody = $this->parseJson($response->getBody(), $response);

if ($jsonBody) {
return $this->parser->parse($member, $jsonBody);
$body = $response->getBody();
if (!$body->isSeekable() || $body->getSize()) {
$jsonBody = $this->parseJson($body, $response);
} else {
$jsonBody = (string) $body;
}

return $this->parser->parse($member, $jsonBody);
}
}
15 changes: 3 additions & 12 deletions aws/aws-sdk-php/src/Api/ErrorParser/RestJsonErrorParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,17 @@ public function __invoke(

// Merge in error data from the JSON body
if ($json = $data['parsed']) {
$data = array_replace($data, $json);
$data = array_replace($json, $data);
}

// Correct error type from services like Amazon Glacier
if (!empty($data['type'])) {
$data['type'] = strtolower($data['type']);
}

// Retrieve the error code from services like Amazon Elastic Transcoder
if ($code = $response->getHeaderLine('x-amzn-errortype')) {
$colon = strpos($code, ':');
$data['code'] = $colon ? substr($code, 0, $colon) : $code;
}

// Retrieve error message directly
$data['message'] = isset($data['parsed']['message'])
? $data['parsed']['message']
: (isset($data['parsed']['Message'])
? $data['parsed']['Message']
: null);
$data['message'] = $data['parsed']['message']
?? ($data['parsed']['Message'] ?? null);

$this->populateShape($data, $response, $command);

Expand Down
46 changes: 39 additions & 7 deletions aws/aws-sdk-php/src/Api/Parser/AbstractRestParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ public function __invoke(
}
}

$body = $response->getBody();
if (!$payload
&& $response->getBody()->getSize() > 0
&& (!$body->isSeekable() || $body->getSize())
&& count($output->getMembers()) > 0
) {
// if no payload was found, then parse the contents of the body
Expand All @@ -73,20 +74,26 @@ private function extractPayload(
array &$result
) {
$member = $output->getMember($payload);
$body = $response->getBody();

if (!empty($member['eventstream'])) {
$result[$payload] = new EventParsingIterator(
$response->getBody(),
$body,
$member,
$this
);
} else if ($member instanceof StructureShape) {
// Structure members parse top-level data into a specific key.
} elseif ($member instanceof StructureShape) {
//Unions must have at least one member set to a non-null value
// If the body is empty, we can assume it is unset
if (!empty($member['union']) && ($body->isSeekable() && !$body->getSize())) {
return;
}

$result[$payload] = [];
$this->payload($response, $member, $result[$payload]);
} else {
// Streaming data is just the stream from the response body.
$result[$payload] = $response->getBody();
// Always set the payload to the body stream, regardless of content
$result[$payload] = $body;
}
}

Expand All @@ -100,13 +107,21 @@ private function extractHeader(
&$result
) {
$value = $response->getHeaderLine($shape['locationName'] ?: $name);
// Empty headers should not be deserialized
if ($value === null || $value === '') {
return;
}

switch ($shape->getType()) {
case 'float':
case 'double':
$value = (float) $value;
$value = match ($value) {
'NaN', 'Infinity', '-Infinity' => $value,
default => (float) $value
};
break;
case 'long':
case 'integer':
$value = (int) $value;
break;
case 'boolean':
Expand Down Expand Up @@ -143,6 +158,23 @@ private function extractHeader(
//output structure.
return;
}
case 'list':
$listMember = $shape->getMember();
$type = $listMember->getType();

// Only boolean lists require special handling
// other types can be returned as-is
if ($type !== 'boolean') {
break;
}

$items = array_map('trim', explode(',', $value));
$value = array_map(
static fn($item) => filter_var($item, FILTER_VALIDATE_BOOLEAN),
$items
);

break;
}

$result[$name] = $value;
Expand Down
5 changes: 4 additions & 1 deletion aws/aws-sdk-php/src/Api/Parser/JsonParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ public function parse(Shape $shape, $value)
$values = $shape->getValue();
$target = [];
foreach ($value as $k => $v) {
$target[$k] = $this->parse($values, $v);
// null map values should not be deserialized
if (!is_null($v)) {
$target[$k] = $this->parse($values, $v);
}
}
return $target;

Expand Down
5 changes: 5 additions & 0 deletions aws/aws-sdk-php/src/Api/Parser/MetadataParserTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,18 @@ protected function extractHeader(
&$result
) {
$value = $response->getHeaderLine($shape['locationName'] ?: $name);
// Empty values should not be deserialized
if ($value === null || $value === '') {
return;
}

switch ($shape->getType()) {
case 'float':
case 'double':
$value = (float) $value;
break;
case 'long':
case 'integer':
$value = (int) $value;
break;
case 'boolean':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,21 @@ protected function readAndHashBytes($num): string
while (!empty($this->tempBuffer) && $num > 0) {
$byte = array_shift($this->tempBuffer);
$bytes .= $byte;
$num = $num - 1;
$num -= 1;
}

// Loop until we've read the expected number of bytes
while ($num > 0 && !$this->stream->eof()) {
$chunk = $this->stream->read($num);
$chunkLen = strlen($chunk);
$bytes .= $chunk;
$num -= $chunkLen;

if ($chunkLen === 0) {
break; // Prevent infinite loop on unexpected EOF
}
}

$bytes = $bytes . $this->stream->read($num);
hash_update($this->hashContext, $bytes);

return $bytes;
Expand Down
10 changes: 9 additions & 1 deletion aws/aws-sdk-php/src/Api/Parser/QueryParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,15 @@ public function __invoke(
ResponseInterface $response
) {
$output = $this->api->getOperation($command->getName())->getOutput();
$xml = $this->parseXml($response->getBody(), $response);
$body = $response->getBody();
$xml = !$body->isSeekable() || $body->getSize()
? $this->parseXml($body, $response)
: null;

// Empty request bodies should not be deserialized.
if (is_null($xml)) {
return new Result();
}

if ($this->honorResultWrapper && $output['resultWrapper']) {
$xml = $xml->{$output['resultWrapper']};
Expand Down
21 changes: 18 additions & 3 deletions aws/aws-sdk-php/src/Api/Parser/RestJsonParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,25 @@ protected function payload(
StructureShape $member,
array &$result
) {
$jsonBody = $this->parseJson($response->getBody(), $response);
$responseBody = (string) $response->getBody();

if ($jsonBody) {
$result += $this->parser->parse($member, $jsonBody);
// Parse JSON if we have content
$parsedJson = null;
if (!empty($responseBody)) {
$parsedJson = $this->parseJson($responseBody, $response);
} else {
// An empty response body should be deserialized as null
$result = $parsedJson;
return;
}

$parsedBody = $this->parser->parse($member, $parsedJson);
if (is_string($parsedBody) && $member['document']) {
// Document types can be strings: replace entire result
$result = $parsedBody;
} else {
// Merge array/object results into existing result
$result = array_merge($result, (array) $parsedBody);
}
}

Expand Down
Loading