diff --git a/src/Api/ErrorParser/AbstractErrorParser.php b/src/Api/ErrorParser/AbstractErrorParser.php
index c72f03f647..887f1ea152 100644
--- a/src/Api/ErrorParser/AbstractErrorParser.php
+++ b/src/Api/ErrorParser/AbstractErrorParser.php
@@ -31,19 +31,6 @@ abstract protected function payload(
StructureShape $member
);
- protected function extractPayload(
- StructureShape $member,
- ResponseInterface $response
- ) {
- if ($member instanceof StructureShape) {
- // Structure members parse top-level data into a specific key.
- return $this->payload($response, $member);
- } else {
- // Streaming data is just the stream from the response body.
- return $response->getBody();
- }
- }
-
protected function populateShape(
array &$data,
ResponseInterface $response,
@@ -57,16 +44,15 @@ protected function populateShape(
if (!empty($data['code'])) {
$errors = $this->api->getOperation($command->getName())->getErrors();
- foreach ($errors as $key => $error) {
+ foreach ($errors as $error) {
// If error code matches a known error shape, populate the body
if ($this->errorCodeMatches($data, $error)) {
- $modeledError = $error;
- $data['body'] = $this->extractPayload(
- $modeledError,
- $response
+ $data['body'] = $this->payload(
+ $response,
+ $error,
);
- $data['error_shape'] = $modeledError;
+ $data['error_shape'] = $error;
foreach ($error->getMembers() as $name => $member) {
switch ($member['location']) {
diff --git a/src/Api/ErrorParser/JsonParserTrait.php b/src/Api/ErrorParser/JsonParserTrait.php
index 67afb1645a..5a28865c96 100644
--- a/src/Api/ErrorParser/JsonParserTrait.php
+++ b/src/Api/ErrorParser/JsonParserTrait.php
@@ -37,10 +37,13 @@ private function genericHandler(ResponseInterface $response): array
);
}
- $parsedBody = null;
- $body = $response->getBody();
- if (!$body->isSeekable() || $body->getSize()) {
- $parsedBody = $this->parseJson((string) $body, $response);
+ // We get the full body content
+ $body = $response->getBody()->getContents();
+
+ $parsedBody = [];
+ // Avoid parsing an empty body
+ if (!empty($body)) {
+ $parsedBody = $this->parseJson($body, $response);
}
// Parse error code from response body
@@ -57,7 +60,7 @@ private function genericHandler(ResponseInterface $response): array
'code' => $error_code ?? null,
'message' => null,
'type' => $error_type,
- 'parsed' => $parsedBody
+ 'parsed' => $parsedBody,
];
}
@@ -132,13 +135,15 @@ protected function payload(
ResponseInterface $response,
StructureShape $member
) {
- $body = $response->getBody();
- if (!$body->isSeekable() || $body->getSize()) {
- $jsonBody = $this->parseJson($body, $response);
- } else {
- $jsonBody = (string) $body;
+ $body = $response->getBody()->getContents();
+
+ // Avoid parsing empty bodies
+ if (empty($body)) {
+ return [];
}
+ $jsonBody = $this->parseJson($body, $response);
+
return $this->parser->parse($member, $jsonBody);
}
}
diff --git a/src/Api/ErrorParser/JsonRpcErrorParser.php b/src/Api/ErrorParser/JsonRpcErrorParser.php
index 35e8ebe0ed..1ed190352a 100644
--- a/src/Api/ErrorParser/JsonRpcErrorParser.php
+++ b/src/Api/ErrorParser/JsonRpcErrorParser.php
@@ -3,6 +3,7 @@
use Aws\Api\Parser\JsonParser;
use Aws\Api\Service;
+use Aws\Api\ResponseWrapper;
use Aws\CommandInterface;
use Psr\Http\Message\ResponseInterface;
@@ -25,23 +26,29 @@ public function __invoke(
ResponseInterface $response,
?CommandInterface $command = null
) {
+ $response = new ResponseWrapper($response);
$data = $this->genericHandler($response);
// Make the casing consistent across services.
- if ($data['parsed']) {
- $data['parsed'] = array_change_key_case($data['parsed']);
+ $parsed = $data['parsed'] ?? null;
+ if ($parsed) {
+ $parsed = array_change_key_case($data['parsed']);
}
- if (isset($data['parsed']['__type'])) {
+ if (isset($parsed['__type'])) {
if (!isset($data['code'])) {
- $parts = explode('#', $data['parsed']['__type']);
- $data['code'] = isset($parts[1]) ? $parts[1] : $parts[0];
+ $parts = explode('#', $parsed['__type']);
+ $data['code'] = $parts[1] ?? $parts[0];
}
- $data['message'] = $data['parsed']['message'] ?? null;
+
+ $data['message'] = $parsed['message'] ?? null;
}
$this->populateShape($data, $response, $command);
+ // Now lets make parsed to be all lowercase
+ $data['parsed'] = $parsed;
+
return $data;
}
}
diff --git a/src/Api/ErrorParser/RestJsonErrorParser.php b/src/Api/ErrorParser/RestJsonErrorParser.php
index 3d50a73ca6..38ea1b4ff4 100644
--- a/src/Api/ErrorParser/RestJsonErrorParser.php
+++ b/src/Api/ErrorParser/RestJsonErrorParser.php
@@ -3,7 +3,7 @@
use Aws\Api\Parser\JsonParser;
use Aws\Api\Service;
-use Aws\Api\StructureShape;
+use Aws\Api\ResponseWrapper;
use Aws\CommandInterface;
use Psr\Http\Message\ResponseInterface;
@@ -26,6 +26,7 @@ public function __invoke(
ResponseInterface $response,
?CommandInterface $command = null
) {
+ $response = new ResponseWrapper($response);
$data = $this->genericHandler($response);
// Merge in error data from the JSON body
@@ -38,9 +39,14 @@ public function __invoke(
$data['type'] = strtolower($data['type']);
}
+ // Make the casing consistent across services.
+ $parsed = $data['parsed'] ?? null;
+ if ($parsed) {
+ $parsed = array_change_key_case($data['parsed']);
+ }
+
// Retrieve error message directly
- $data['message'] = $data['parsed']['message']
- ?? ($data['parsed']['Message'] ?? null);
+ $data['message'] = $parsed['message'] ?? null;
$this->populateShape($data, $response, $command);
diff --git a/src/Api/ErrorParser/XmlErrorParser.php b/src/Api/ErrorParser/XmlErrorParser.php
index 86f5d0be54..91aadf0f90 100644
--- a/src/Api/ErrorParser/XmlErrorParser.php
+++ b/src/Api/ErrorParser/XmlErrorParser.php
@@ -5,6 +5,7 @@
use Aws\Api\Parser\XmlParser;
use Aws\Api\Service;
use Aws\Api\StructureShape;
+use Aws\Api\ResponseWrapper;
use Aws\CommandInterface;
use Psr\Http\Message\ResponseInterface;
@@ -27,6 +28,7 @@ public function __invoke(
ResponseInterface $response,
?CommandInterface $command = null
) {
+ $response = new ResponseWrapper($response);
$code = (string) $response->getStatusCode();
$data = [
@@ -34,11 +36,14 @@ public function __invoke(
'request_id' => null,
'code' => null,
'message' => null,
- 'parsed' => null
+ 'parsed' => null,
];
- $body = $response->getBody();
- if ($body->getSize() > 0) {
+ // Get the full body content
+ $body = $response->getBody()->getContents();
+
+ // Parse just if is not empty
+ if (!empty($body)) {
$this->parseBody($this->parseXml($body, $response), $data);
} else {
$this->parseHeaders($response, $data);
@@ -100,12 +105,27 @@ protected function payload(
ResponseInterface $response,
StructureShape $member
) {
- $xmlBody = $this->parseXml($response->getBody(), $response);
+ $body = $response->getBody()->getContents();
+
+ // Avoid parsing empty bodies
+ if (empty($body)) {
+ return [];
+ }
+
+ $xmlBody = $this->parseXml(
+ $body,
+ $response
+ );
+
$prefix = $this->registerNamespacePrefix($xmlBody);
$errorBody = $xmlBody->xpath("//{$prefix}Error");
if (is_array($errorBody) && !empty($errorBody[0])) {
return $this->parser->parse($member, $errorBody[0]);
}
+
+ // Fallback since we should either throw an exception or return a value
+ // when the condition above is not met.
+ return [];
}
}
diff --git a/src/Api/Operation.php b/src/Api/Operation.php
index 36ba3950cb..43c695e72b 100644
--- a/src/Api/Operation.php
+++ b/src/Api/Operation.php
@@ -89,7 +89,7 @@ public function getOutput()
/**
* Get an array of operation error shapes.
*
- * @return Shape[]
+ * @return StructureShape[]
*/
public function getErrors()
{
diff --git a/src/Api/Parser/AbstractRestParser.php b/src/Api/Parser/AbstractRestParser.php
index a0267e8d72..4898d6475a 100644
--- a/src/Api/Parser/AbstractRestParser.php
+++ b/src/Api/Parser/AbstractRestParser.php
@@ -2,6 +2,7 @@
namespace Aws\Api\Parser;
use Aws\Api\DateTimeResult;
+use Aws\Api\ResponseWrapper;
use Aws\Api\Shape;
use Aws\Api\StructureShape;
use Aws\Result;
@@ -55,9 +56,16 @@ public function __invoke(
}
}
- $body = $response->getBody();
+ // Wrap response to allow the body
+ // to be read multiple times
+ // even in non-seekable streams.
+ $response = new ResponseWrapper($response);
+
+ // Get the body content to know whether is empty
+ $body = $response->getBody()->getContents();
+ // Make sure empty payloads are not parsed
if (!$payload
- && (!$body->isSeekable() || $body->getSize())
+ && !empty($body)
&& count($output->getMembers()) > 0
) {
// if no payload was found, then parse the contents of the body
@@ -75,17 +83,25 @@ private function extractPayload(
) {
$member = $output->getMember($payload);
$body = $response->getBody();
-
if (!empty($member['eventstream'])) {
$result[$payload] = new EventParsingIterator(
$body,
$member,
$this
);
- } elseif ($member instanceof StructureShape) {
+
+ return;
+ }
+
+ // We don't create the wrapper above
+ // because on event-stream we want to keep
+ // the original non-seekable stream.
+ $response = new ResponseWrapper($response);
+ if ($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())) {
+ if (!empty($member['union'])
+ && empty($response->getBody()->getContents())) {
return;
}
diff --git a/src/Api/Parser/QueryParser.php b/src/Api/Parser/QueryParser.php
index 2ea0676675..52ef5a7ed2 100644
--- a/src/Api/Parser/QueryParser.php
+++ b/src/Api/Parser/QueryParser.php
@@ -40,8 +40,10 @@ public function __invoke(
ResponseInterface $response
) {
$output = $this->api->getOperation($command->getName())->getOutput();
- $body = $response->getBody();
- $xml = !$body->isSeekable() || $body->getSize()
+ // Read the full payload, even in non-seekable streams
+ $body = $response->getBody()->getContents();
+ // Just parse when the body is not empty
+ $xml = !empty($body)
? $this->parseXml($body, $response)
: null;
diff --git a/src/Api/ResponseWrapper.php b/src/Api/ResponseWrapper.php
new file mode 100644
index 0000000000..2c27b0cb91
--- /dev/null
+++ b/src/Api/ResponseWrapper.php
@@ -0,0 +1,46 @@
+getBody();
+ if (!$body->isSeekable()) {
+ $body = new CachingStream($response->getBody());
+ }
+
+ parent::__construct(
+ $response->getStatusCode(),
+ $response->getHeaders(),
+ $body,
+ $response->getProtocolVersion(),
+ $response->getReasonPhrase()
+ );
+ }
+
+ /**
+ * @return StreamInterface
+ */
+ public function getBody(): StreamInterface
+ {
+ $stream = parent::getBody();
+ $stream->rewind();
+
+ return $stream;
+ }
+}
diff --git a/src/AwsClient.php b/src/AwsClient.php
index 2b860b2211..b29c1ac061 100644
--- a/src/AwsClient.php
+++ b/src/AwsClient.php
@@ -283,6 +283,7 @@ public function __construct(array $args)
$args['with_resolved']($config);
}
$this->addUserAgentMiddleware($config);
+ $this->addEventStreamHttpFlagMiddleware();
}
public function getHandlerList()
@@ -643,6 +644,33 @@ private function addUserAgentMiddleware($args)
);
}
+ /**
+ * Enables streaming the response by using the stream flag.
+ *
+ * @return void
+ */
+ private function addEventStreamHttpFlagMiddleware(): void
+ {
+ $this->getHandlerList()
+ -> appendInit(
+ function (callable $handler) {
+ return function (CommandInterface $command, $request = null) use ($handler) {
+ $operation = $this->getApi()->getOperation($command->getName());
+ $output = $operation->getOutput();
+ foreach ($output->getMembers() as $memberProps) {
+ if (!empty($memberProps['eventstream'])) {
+ $command['@http']['stream'] = true;
+ break;
+ }
+ }
+
+ return $handler($command, $request);
+ };
+ },
+ 'event-streaming-flag-middleware'
+ );
+ }
+
/**
* Retrieves client context param definition from service model,
* creates mapping of client context param names with client-provided
diff --git a/src/CloudWatchLogs/CloudWatchLogsClient.php b/src/CloudWatchLogs/CloudWatchLogsClient.php
index 8023c0029a..284924c135 100644
--- a/src/CloudWatchLogs/CloudWatchLogsClient.php
+++ b/src/CloudWatchLogs/CloudWatchLogsClient.php
@@ -192,36 +192,10 @@
* @method \GuzzleHttp\Promise\Promise updateLogAnomalyDetectorAsync(array $args = [])
*/
class CloudWatchLogsClient extends AwsClient {
- static $streamingCommands = [
- 'StartLiveTail' => true
- ];
public function __construct(array $args)
{
parent::__construct($args);
- $this->addStreamingFlagMiddleware();
- }
-
- private function addStreamingFlagMiddleware()
- {
- $this->getHandlerList()
- -> appendInit(
- $this->getStreamingFlagMiddleware(),
- 'streaming-flag-middleware'
- );
- }
-
- private function getStreamingFlagMiddleware(): callable
- {
- return function (callable $handler) {
- return function (CommandInterface $command, $request = null) use ($handler) {
- if (!empty(self::$streamingCommands[$command->getName()])) {
- $command['@http']['stream'] = true;
- }
-
- return $handler($command, $request);
- };
- };
}
/**
diff --git a/src/WrappedHttpHandler.php b/src/WrappedHttpHandler.php
index 1c602de494..c6cfa3260c 100644
--- a/src/WrappedHttpHandler.php
+++ b/src/WrappedHttpHandler.php
@@ -166,10 +166,11 @@ private function parseError(
throw new \RuntimeException('The HTTP handler was rejected without an "exception" key value pair.');
}
- $serviceError = "AWS HTTP error: " . $err['exception']->getMessage();
+ $serviceError = "AWS HTTP error:\n";
if (!isset($err['response'])) {
$parts = ['response' => null];
+ $serviceError .= $err['exception']->getMessage();
} else {
try {
$parts = call_user_func(
@@ -177,8 +178,9 @@ private function parseError(
$err['response'],
$command
);
- $serviceError .= " {$parts['code']} ({$parts['type']}): "
- . "{$parts['message']} - " . $err['response']->getBody();
+
+ $serviceError .= "{$parts['code']} ({$parts['type']}): "
+ . "{$parts['message']}";
} catch (ParserException $e) {
$parts = [];
$serviceError .= ' Unable to parse error information from '
diff --git a/tests/Api/Parser/RestJsonParserTest.php b/tests/Api/Parser/RestJsonParserTest.php
index 132c2cc35d..6f6b64e7bd 100644
--- a/tests/Api/Parser/RestJsonParserTest.php
+++ b/tests/Api/Parser/RestJsonParserTest.php
@@ -6,7 +6,9 @@
use Aws\Api\Service;
use Aws\CommandInterface;
use Aws\Test\Api\Parser\ParserTestServiceTrait;
+use GuzzleHttp\Psr7\NoSeekStream;
use GuzzleHttp\Psr7\Response;
+use GuzzleHttp\Psr7\Utils;
use Yoast\PHPUnitPolyfills\TestCases\TestCase;
/**
@@ -180,4 +182,49 @@ public function testParsesFalsyHeaderValues(): void
// Empty string should still be skipped
$this->assertArrayNotHasKey('Empty', $result);
}
+
+ public function testParsesEmptyResponseOnNonSeekableStream(): void
+ {
+ $shape = [
+ 'type' => 'structure',
+ 'members' => [
+ 'TestMember' => [
+ 'type' => 'string',
+ ]
+ ]
+ ];
+
+ $api = new Service([
+ 'metadata' => [
+ 'protocol' => 'rest-json'
+ ],
+ 'operations' => [
+ 'TestOperation' => [
+ 'http' => ['method' => 'GET'],
+ 'output' => $shape
+ ]
+ ],
+ 'shapes' => []
+ ], function () {});
+
+ $parser = new RestJsonParser($api);
+ $response = new Response(
+ 200,
+ [],
+ new NoSeekStream(
+ Utils::streamFor()
+ )
+ );
+ $command = $this->getMockBuilder(
+ CommandInterface::class
+ )->getMock();
+ $command->method('getName')->willReturn('TestOperation');
+
+ $parser($command, $response);
+
+ // Not error occurred. Test successfully.
+ // Previously, on non-seekable streams it would have failed.
+ // Because, it would have tried to parse an empty string.
+ $this->assertTrue(true);
+ }
}
diff --git a/tests/Api/Parser/RestXmlParserTest.php b/tests/Api/Parser/RestXmlParserTest.php
index 3a968373ab..b586ad40b4 100644
--- a/tests/Api/Parser/RestXmlParserTest.php
+++ b/tests/Api/Parser/RestXmlParserTest.php
@@ -5,7 +5,9 @@
use Aws\Api\Parser\RestXmlParser;
use Aws\Api\Service;
use Aws\CommandInterface;
+use GuzzleHttp\Psr7\NoSeekStream;
use GuzzleHttp\Psr7\Response;
+use GuzzleHttp\Psr7\Utils;
use Yoast\PHPUnitPolyfills\TestCases\TestCase;
/**
@@ -110,4 +112,49 @@ public function testParsesFalsyHeaderValues(): void
// Empty string should still be skipped
$this->assertArrayNotHasKey('Empty', $result);
}
+
+ public function testParsesEmptyResponseOnNonSeekableStream(): void
+ {
+ $shape = [
+ 'type' => 'structure',
+ 'members' => [
+ 'TestMember' => [
+ 'type' => 'string',
+ ]
+ ]
+ ];
+
+ $api = new Service([
+ 'metadata' => [
+ 'protocol' => 'rest-xml'
+ ],
+ 'operations' => [
+ 'TestOperation' => [
+ 'http' => ['method' => 'GET'],
+ 'output' => $shape
+ ]
+ ],
+ 'shapes' => []
+ ], function () {});
+
+ $parser = new RestXmlParser($api);
+ $response = new Response(
+ 200,
+ [],
+ new NoSeekStream(
+ Utils::streamFor()
+ )
+ );
+ $command = $this->getMockBuilder(
+ CommandInterface::class
+ )->getMock();
+ $command->method('getName')->willReturn('TestOperation');
+
+ $parser($command, $response);
+
+ // Not error occurred. Test successfully.
+ // Previously, on non-seekable streams it would have failed.
+ // Because, it would have tried to parse an empty string.
+ $this->assertTrue(true);
+ }
}
diff --git a/tests/AwsClientTest.php b/tests/AwsClientTest.php
index 2f612faaf5..d546b70937 100644
--- a/tests/AwsClientTest.php
+++ b/tests/AwsClientTest.php
@@ -3,6 +3,7 @@
use Aws\Api\ApiProvider;
use Aws\Api\ErrorParser\JsonRpcErrorParser;
+use Aws\Api\Service;
use Aws\AwsClient;
use Aws\CommandInterface;
use Aws\Credentials\Credentials;
@@ -22,6 +23,7 @@
use Aws\Token\Token;
use Aws\Waiter;
use Aws\WrappedHttpHandler;
+use Exception;
use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Promise\RejectedPromise;
use GuzzleHttp\Psr7\Response;
@@ -99,14 +101,14 @@ public function testReturnsCommandForOperation()
public function testWrapsExceptions()
{
- $this->expectExceptionMessage("Error executing \"foo\" on \"http://us-east-1.foo.amazonaws.com/\"; AWS HTTP error: Baz Bar!");
- $this->expectException(\Aws\S3\Exception\S3Exception::class);
+ $this->expectExceptionMessage("Error executing \"foo\" on \"http://us-east-1.foo.amazonaws.com/\"; AWS HTTP error:\nBaz Bar!");
+ $this->expectException(S3Exception::class);
$parser = function () {};
$errorParser = new JsonRpcErrorParser();
$h = new WrappedHttpHandler(
function () {
return new RejectedPromise([
- 'exception' => new \Exception('Baz Bar!'),
+ 'exception' => new Exception('Baz Bar!'),
'connection_error' => true,
'response' => null
]);
@@ -1021,4 +1023,137 @@ public function testAppendsUserAgentMiddleware()
]);
$client->listBuckets();
}
+
+ /**
+ * @dataProvider appendEventStreamFlagMiddlewareProvider
+ *
+ * @param array $definition
+ * @param bool $isFlagPresent
+ *
+ * @return void
+ *
+ * @throws Exception
+ */
+ public function testAppendEventStreamHttpFlagMiddleware(
+ array $definition,
+ bool $isFlagPresent
+ ): void
+ {
+
+ $service = new Service($definition, function () {});
+ $client = new AwsClient([
+ 'service' => 'TestService',
+ 'api_provider' => function () use ($service) {
+ return $service->toArray();
+ },
+ 'region' => 'us-east-1',
+ 'version' => 'latest',
+ ]);
+ $called = false;
+ $client->getHandlerList()->setHandler(new MockHandler([new Result()]));
+ $client->getHandlerList()->appendInit(function(callable $handler)
+ use ($isFlagPresent, &$called) {
+ return function (CommandInterface $command, ?RequestInterface $request = null)
+ use ($handler, $isFlagPresent, &$called) {
+ $called = true;
+ $this->assertTrue(
+ ($command['@http']['stream'] ?? false) === $isFlagPresent,
+ );
+
+ return $handler($command, $request);
+ };
+ });
+
+ $command = $client->getCommand('OperationTest');
+ $client->execute($command);
+ $this->assertTrue($called);
+ }
+
+ /**
+ * @return array[]
+ */
+ public function appendEventStreamFlagMiddlewareProvider(): array
+ {
+ return [
+ 'service_with_flag_present' => [
+ 'definition' => [
+ 'metadata' => [
+ 'protocol' => 'rest-json',
+ 'protocols' => [
+ 'rest-json'
+ ]
+ ],
+ 'operations' => [
+ 'OperationTest' => [
+ 'name' => 'OperationTest',
+ 'http' => [
+ 'method' => 'POST',
+ 'requestUri' => '/operationTest',
+ 'responseCode' => 200,
+ ],
+ 'input' => ['shape' => 'OperationTestInput'],
+ 'output' => ['shape' => 'OperationTestOutput'],
+ ]
+ ],
+ 'shapes' => [
+ 'OperationTestInput' => [
+ 'type' => 'structure',
+ 'members' => [
+ ]
+ ],
+ 'OperationTestOutput' => [
+ 'type' => 'structure',
+ 'members' => [
+ 'Stream' => [
+ 'shape' => 'StreamShape',
+ 'eventstream' => true
+ ]
+ ]
+ ],
+ 'StreamShape' => ['type' => 'structure']
+ ]
+ ],
+ 'present' => true
+ ],
+ 'service_with_flag_no_present' => [
+ 'definition' => [
+ 'metadata' => [
+ 'protocol' => 'rest-json',
+ 'protocols' => [
+ 'rest-json'
+ ]
+ ],
+ 'operations' => [
+ 'OperationTest' => [
+ 'name' => 'OperationTest',
+ 'http' => [
+ 'method' => 'POST',
+ 'requestUri' => '/operationTest',
+ 'responseCode' => 200,
+ ],
+ 'input' => ['shape' => 'OperationTestInput'],
+ 'output' => ['shape' => 'OperationTestOutput'],
+ ]
+ ],
+ 'shapes' => [
+ 'OperationTestInput' => [
+ 'type' => 'structure',
+ 'members' => [
+ ]
+ ],
+ 'OperationTestOutput' => [
+ 'type' => 'structure',
+ 'members' => [
+ 'NoStream' => [
+ 'shape' => 'NoStreamShape',
+ ]
+ ]
+ ],
+ 'NoStreamShape' => ['type' => 'structure']
+ ]
+ ],
+ 'present' => false
+ ]
+ ];
+ }
}
diff --git a/tests/WrappedHttpHandlerTest.php b/tests/WrappedHttpHandlerTest.php
index 9311b47ae6..f813f4089b 100644
--- a/tests/WrappedHttpHandlerTest.php
+++ b/tests/WrappedHttpHandlerTest.php
@@ -11,6 +11,8 @@
use Aws\Result;
use Aws\WrappedHttpHandler;
use GuzzleHttp\Promise\RejectedPromise;
+use GuzzleHttp\Psr7\NoSeekStream;
+use GuzzleHttp\Psr7\Utils;
use Psr\Http\Message\RequestInterface;
use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Psr7\Response;
@@ -375,4 +377,84 @@ public function testPassesOnTransferStatsCallbackToHandlerWhenRequested()
$wrapped(new Command('a'), new Request('GET', 'http://foo.com'))
->wait();
}
+
+ /**
+ * @dataProvider errorIsParsedOnNonSeekableResponseBodyProvider
+ *
+ * @return void
+ */
+ public function testErrorIsParsedOnNonSeekableResponseBody(
+ string $protocol,
+ string $body,
+ string $expected
+ )
+ {
+ $service = $this->generateTestService($protocol);
+ $parser = Service::createParser($service);
+ $errorParser = Service::createErrorParser($service->getProtocol(), $service);
+ $client = $this->generateTestClient(
+ $service
+ );
+ $command = $client->getCommand('TestOperation');
+ $exception = new AwsException(
+ 'Failed performing test operation',
+ $command,
+ );
+ $uri = 'http://myservice.myregion.foo.com';
+ $request = new Request('GET', $uri);
+ $response = new Response(
+ 403,
+ [],
+ new NoSeekStream(
+ Utils::streamFor($body)
+ )
+ );
+ $handler = function () use ($exception, $response) {
+ return new RejectedPromise([
+ 'exception' => $exception,
+ 'response' => $response,
+ ]);
+ };
+ $wrapped = new WrappedHttpHandler($handler, $parser, $errorParser);
+ try {
+ $wrapped($command, $request)->wait();
+ $this->fail(
+ "Operation should have failed!"
+ );
+ } catch (\Exception $exception) {
+ $this->assertStringContainsString(
+ $expected,
+ $exception->getMessage()
+ );
+ }
+ }
+
+ /**
+ * @return array[]
+ */
+ public function errorIsParsedOnNonSeekableResponseBodyProvider(): array
+ {
+ return [
+ 'json' => [
+ 'protocol' => 'json',
+ 'body' => '{"Message": "Action not allowed!", "__Type": "ListObjects"}',
+ 'expected' => 'ListObjects (client): Action not allowed!',
+ ],
+ 'query' => [
+ 'protocol' => 'query',
+ 'body' => 'ListObjectsAction not allowed!',
+ 'expected' => 'ListObjects (client): Action not allowed!',
+ ],
+ 'rest-xml' => [
+ 'protocol' => 'rest-xml',
+ 'body' => 'ListObjectsAction not allowed!',
+ 'expected' => 'ListObjects (client): Action not allowed!',
+ ],
+ 'rest-json' => [
+ 'protocol' => 'rest-json',
+ 'body' => '{"message": "Action not allowed!", "code": "ListObjects"}',
+ 'expected' => 'ListObjects (client): Action not allowed!',
+ ]
+ ];
+ }
}