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!', + ] + ]; + } }