Skip to content
Open
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
5 changes: 0 additions & 5 deletions src/Api/DateTimeResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,6 @@ public static function fromEpoch($unixTimestamp)
throw new ParserException('Invalid timestamp value passed to DateTimeResult::fromEpoch');
}

// PHP 5.5 does not support sub-second precision
if (\PHP_VERSION_ID < 56000) {
return new self(gmdate('c', $unixTimestamp));
}

$decimalSeparator = isset(localeconv()['decimal_point']) ? localeconv()['decimal_point'] : ".";
$formatString = "U" . $decimalSeparator . "u";
$dateTime = DateTime::createFromFormat(
Expand Down
161 changes: 161 additions & 0 deletions src/Api/ErrorParser/AbstractRpcV2ErrorParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
<?php
namespace Aws\Api\ErrorParser;

use Aws\Api\Service;
use Aws\Api\StructureShape;
use Aws\CommandInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;

/**
* Base implementation for Smithy RPC V2 protocol error parsers.
*
* @internal
*/
abstract class AbstractRpcV2ErrorParser extends AbstractErrorParser
{
private const HEADER_QUERY_ERROR = 'x-amzn-query-error';
private const HEADER_ERROR_TYPE = 'x-amzn-errortype';
private const HEADER_REQUEST_ID = 'x-amzn-requestid';

/**
* @param ResponseInterface $response
* @param CommandInterface|null $command
*
* @return array
*/
public function __invoke(
ResponseInterface $response,
?CommandInterface $command = null
) {
$data = $this->parseError($response);
if ($data['parsed']) {
$data['parsed'] = array_change_key_case($data['parsed']);
}

if (isset($data['parsed']['__type'])) {
$data['code'] ??= $this->extractErrorCode($data['parsed']['__type']);
$data['message'] = $data['parsed']['message'] ?? null;
}

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

return $data;
}

/**
* @param ResponseInterface $response
* @param StructureShape $member
*
* @return array
*/
abstract protected function payload(
ResponseInterface $response,
StructureShape $member
): array;

/**
* @param StreamInterface $body
* @param ResponseInterface $response
*
* @return mixed
*/
abstract protected function parseBody(
StreamInterface $body,
ResponseInterface $response
): mixed;

/**
* @param ResponseInterface $response
*
* @return array
*/
private function parseError(ResponseInterface $response): array
{
$statusCode = (string) $response->getStatusCode();
$errorCode = null;
$errorType = null;

if ($this->api?->getMetadata('awsQueryCompatible') !== null
&& $response->hasHeader(self::HEADER_QUERY_ERROR)
&& $awsQueryError = $this->parseQueryCompatibleHeader($response)
) {
$errorCode = $awsQueryError['code'];
$errorType = $awsQueryError['type'];
}

if (!$errorCode && $response->hasHeader(self::HEADER_ERROR_TYPE)) {
$errorCode = $this->extractErrorCode(
$response->getHeaderLine(self::HEADER_ERROR_TYPE)
);
}

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

if (!$errorCode && $parsedBody) {
$errorCode = $this->extractErrorCode(
$parsedBody['code'] ?? $parsedBody['__type'] ?? ''
);
}

return [
'request_id' => $response->getHeaderLine(self::HEADER_REQUEST_ID),
'code' => $errorCode ?: null,
'message' => null,
'type' => $errorType ?? ($statusCode[0] === '4' ? 'client' : 'server'),
'parsed' => $parsedBody,
];
}

/**
* Parse AWS Query Compatible error from header
*
* @param ResponseInterface $response
*
* @return array|null Returns ['code' => string, 'type' => string] or null
*/
private function parseQueryCompatibleHeader(ResponseInterface $response): ?array
{
$parts = explode(';', $response->getHeaderLine(self::HEADER_QUERY_ERROR));
if (count($parts) === 2 && $parts[0] && $parts[1]) {
return [
'code' => $parts[0],
'type' => $parts[1],
];
}

return null;
}

/**
* 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#ErrorCode: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;
}
}
68 changes: 68 additions & 0 deletions src/Api/ErrorParser/RpcV2CborErrorParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php
namespace Aws\Api\ErrorParser;

use Aws\Api\Parser\RpcV2ParserTrait;
use Aws\Api\Service;
use Aws\Api\StructureShape;
use Aws\Cbor\CborDecoder;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;

/**
* Parses errors according to Smithy RPC V2 CBOR protocol standards.
*
* https://smithy.io/2.0/additional-specs/protocols/smithy-rpc-v2.html
*
* @internal
*/
final class RpcV2CborErrorParser extends AbstractRpcV2ErrorParser
{
/** @var CborDecoder */
private CborDecoder $decoder;

use RpcV2ParserTrait;

/**
* @param Service|null $api
* @param CborDecoder|null $decoder
*/
public function __construct(
?Service $api = null,
?CborDecoder $decoder = null
) {
$this->decoder = $decoder ?: new CborDecoder();
parent::__construct($api);
}

/**
* @param ResponseInterface $response
* @param StructureShape $member
*
* @return array
* @throws \Exception
*/
protected function payload(
ResponseInterface $response,
StructureShape $member
): array
{
$body = $response->getBody();
$cborBody = $this->parseCbor($body, $response);

return $this->resolveOutputShape($member, $cborBody);
}

/**
* @param StreamInterface $body
* @param ResponseInterface $response
*
* @return mixed
*/
protected function parseBody(
StreamInterface $body,
ResponseInterface $response
): mixed
{
return $this->parseCbor($body, $response);
}
}
83 changes: 83 additions & 0 deletions src/Api/Parser/AbstractRpcV2Parser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php
namespace Aws\Api\Parser;

use Aws\Api\Operation;
use Aws\Api\Parser\Exception\ParserException;
use Aws\Result;
use Aws\CommandInterface;
use Psr\Http\Message\ResponseInterface;

/**
* Base implementation for Smithy RPC V2 protocol parsers.
*
* Implementers MUST define the following static property representing
* the `Smithy-Protocol` header value:
* self::HEADER_SMITHY_PROTOCOL => static::$smithyProtocol
*
* @internal
*/
abstract class AbstractRpcV2Parser extends AbstractParser
{
private const HEADER_SMITHY_PROTOCOL = 'Smithy-Protocol';

/** @var string */
protected static string $smithyProtocol;

public function __invoke(
CommandInterface $command,
ResponseInterface $response
) {
$operation = $this->api->getOperation($command->getName());

return $this->parseResponse($response, $operation);
}

/**
* Parses a response according to Smithy RPC V2 protocol standards.
*
* @param ResponseInterface $response the response to parse.
* @param Operation $operation the operation which holds information for
* parsing the response.
*
* @return Result
*/
private function parseResponse(
ResponseInterface $response,
Operation $operation
): Result
{
$smithyProtocolHeader = $response->getHeaderLine(self::HEADER_SMITHY_PROTOCOL);
if ($smithyProtocolHeader !== static::$smithyProtocol) {
$statusCode = $response->getStatusCode();
throw new ParserException(
"Malformed response: Smithy-Protocol header mismatch (HTTP {$statusCode}). "
. 'Expected ' . static::$smithyProtocol
);
}

if ($operation['output'] === null) {
return new Result([]);
}

$outputShape = $operation->getOutput();
foreach ($outputShape->getMembers() as $memberName => $memberProps) {
if (!empty($memberProps['eventstream'])) {
return new Result([
$memberName => new EventParsingIterator(
$response->getBody(),
$outputShape->getMember($memberName),
$this
)
]);
}
}

$result = $this->parseMemberFromStream(
$response->getBody(),
$outputShape,
$response
);

return new Result(is_null($result) ? [] : $result);
}
}
1 change: 0 additions & 1 deletion src/Api/Parser/PayloadParserTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
namespace Aws\Api\Parser;

use Aws\Api\Parser\Exception\ParserException;
use Psr\Http\Message\ResponseInterface;

trait PayloadParserTrait
{
Expand Down
54 changes: 54 additions & 0 deletions src/Api/Parser/RpcV2CborParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php
namespace Aws\Api\Parser;

use Aws\Api\StructureShape;
use Aws\Api\Service;
use Aws\Cbor\CborDecoder;
use Psr\Http\Message\StreamInterface;

/**
* Parses responses according to Smithy RPC V2 CBOR protocol standards.
*
* https://smithy.io/2.0/additional-specs/protocols/smithy-rpc-v2.html
*
* @internal
*/
final class RpcV2CborParser extends AbstractRpcV2Parser
{
/** @var string */
protected static string $smithyProtocol = 'rpc-v2-cbor';

/** @var CborDecoder */
private CborDecoder $decoder;

use RpcV2ParserTrait;

/**
* @param Service $api Service description
* @param CborDecoder|null $decoder Used to decode CBOR-encoded response data
*/
public function __construct(
Service $api,
?CborDecoder $decoder = null
)
{
$this->decoder = $decoder ?? new CborDecoder();
parent::__construct($api);
}

/**
* @param StreamInterface $stream
* @param StructureShape $member
* @param $response
*
* @return mixed
*/
public function parseMemberFromStream(
StreamInterface $stream,
StructureShape $member,
$response
): mixed
{
return $this->resolveOutputShape($member, $this->parseCbor($stream, $response));
}
}
Loading