GuzzleHandler.php 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. <?php
  2. namespace Aws\Handler\GuzzleV5;
  3. use Exception;
  4. use GuzzleHttp\Client;
  5. use GuzzleHttp\ClientInterface;
  6. use GuzzleHttp\Event\EndEvent;
  7. use GuzzleHttp\Exception\ConnectException;
  8. use GuzzleHttp\Exception\RequestException;
  9. use GuzzleHttp\Message\ResponseInterface as GuzzleResponse;
  10. use GuzzleHttp\Promise;
  11. use GuzzleHttp\Psr7\Response as Psr7Response;
  12. use GuzzleHttp\Stream\Stream;
  13. use Psr\Http\Message\RequestInterface as Psr7Request;
  14. use Psr\Http\Message\StreamInterface as Psr7StreamInterface;
  15. /**
  16. * A request handler that sends PSR-7-compatible requests with Guzzle 5.
  17. *
  18. * The handler accepts a PSR-7 Request object and an array of transfer options
  19. * and returns a Guzzle 6 Promise. The promise is either resolved with a
  20. * PSR-7 Response object or rejected with an array of error data.
  21. *
  22. * @codeCoverageIgnore
  23. */
  24. class GuzzleHandler
  25. {
  26. private static $validOptions = [
  27. 'proxy' => true,
  28. 'expect' => true,
  29. 'cert' => true,
  30. 'verify' => true,
  31. 'timeout' => true,
  32. 'debug' => true,
  33. 'connect_timeout' => true,
  34. 'stream' => true,
  35. 'delay' => true,
  36. 'sink' => true,
  37. ];
  38. /** @var ClientInterface */
  39. private $client;
  40. /**
  41. * @param ClientInterface $client
  42. */
  43. public function __construct(ClientInterface $client = null)
  44. {
  45. $this->client = $client ?: new Client();
  46. }
  47. /**
  48. * @param Psr7Request $request
  49. * @param array $options
  50. * @return Promise\Promise|Promise\PromiseInterface
  51. * @throws \GuzzleHttp\Exception\GuzzleException
  52. */
  53. public function __invoke(Psr7Request $request, array $options = [])
  54. {
  55. // Create and send a Guzzle 5 request
  56. $guzzlePromise = $this->client->send(
  57. $this->createGuzzleRequest($request, $options)
  58. );
  59. $promise = new Promise\Promise(
  60. function () use ($guzzlePromise) {
  61. try {
  62. $guzzlePromise->wait();
  63. } catch (\Exception $e) {
  64. // The promise is already delivered when the exception is
  65. // thrown, so don't rethrow it.
  66. }
  67. },
  68. [$guzzlePromise, 'cancel']
  69. );
  70. $guzzlePromise->then([$promise, 'resolve'], [$promise, 'reject']);
  71. return $promise->then(
  72. function (GuzzleResponse $response) {
  73. // Adapt the Guzzle 5 Future to a Guzzle 6 ResponsePromise.
  74. return $this->createPsr7Response($response);
  75. },
  76. function (Exception $exception) use ($options) {
  77. // If we got a 'sink' that's a path, set the response body to
  78. // the contents of the file. This will build the resulting
  79. // exception with more information.
  80. if ($exception instanceof RequestException) {
  81. if (isset($options['sink'])) {
  82. if (!($options['sink'] instanceof Psr7StreamInterface)) {
  83. $exception->getResponse()->setBody(
  84. Stream::factory(
  85. file_get_contents($options['sink'])
  86. )
  87. );
  88. }
  89. }
  90. }
  91. // Reject with information about the error.
  92. return new Promise\RejectedPromise($this->prepareErrorData($exception));
  93. }
  94. );
  95. }
  96. private function createGuzzleRequest(Psr7Request $psrRequest, array $options)
  97. {
  98. $ringConfig = [];
  99. $statsCallback = isset($options['http_stats_receiver'])
  100. ? $options['http_stats_receiver']
  101. : null;
  102. unset($options['http_stats_receiver']);
  103. // Remove unsupported options.
  104. foreach (array_keys($options) as $key) {
  105. if (!isset(self::$validOptions[$key])) {
  106. unset($options[$key]);
  107. }
  108. }
  109. // Handle delay option.
  110. if (isset($options['delay'])) {
  111. $ringConfig['delay'] = $options['delay'];
  112. unset($options['delay']);
  113. }
  114. // Prepare sink option.
  115. if (isset($options['sink'])) {
  116. $ringConfig['save_to'] = ($options['sink'] instanceof Psr7StreamInterface)
  117. ? new GuzzleStream($options['sink'])
  118. : $options['sink'];
  119. unset($options['sink']);
  120. }
  121. // Ensure that all requests are async and lazy like Guzzle 6.
  122. $options['future'] = 'lazy';
  123. // Create the Guzzle 5 request from the provided PSR7 request.
  124. $request = $this->client->createRequest(
  125. $psrRequest->getMethod(),
  126. $psrRequest->getUri(),
  127. $options
  128. );
  129. if (is_callable($statsCallback)) {
  130. $request->getEmitter()->on(
  131. 'end',
  132. function (EndEvent $event) use ($statsCallback) {
  133. $statsCallback($event->getTransferInfo());
  134. }
  135. );
  136. }
  137. // For the request body, adapt the PSR stream to a Guzzle stream.
  138. $body = $psrRequest->getBody();
  139. if ($body->getSize() === 0) {
  140. $request->setBody(null);
  141. } else {
  142. $request->setBody(new GuzzleStream($body));
  143. }
  144. $request->setHeaders($psrRequest->getHeaders());
  145. $request->setHeader(
  146. 'User-Agent',
  147. $request->getHeader('User-Agent')
  148. . ' ' . Client::getDefaultUserAgent()
  149. );
  150. // Make sure the delay is configured, if provided.
  151. if ($ringConfig) {
  152. foreach ($ringConfig as $k => $v) {
  153. $request->getConfig()->set($k, $v);
  154. }
  155. }
  156. return $request;
  157. }
  158. private function createPsr7Response(GuzzleResponse $response)
  159. {
  160. if ($body = $response->getBody()) {
  161. $body = new PsrStream($body);
  162. }
  163. return new Psr7Response(
  164. $response->getStatusCode(),
  165. $response->getHeaders(),
  166. $body,
  167. $response->getReasonPhrase()
  168. );
  169. }
  170. private function prepareErrorData(Exception $e)
  171. {
  172. $error = [
  173. 'exception' => $e,
  174. 'connection_error' => false,
  175. 'response' => null,
  176. ];
  177. if ($e instanceof ConnectException) {
  178. $error['connection_error'] = true;
  179. }
  180. if ($e instanceof RequestException && $e->getResponse()) {
  181. $error['response'] = $this->createPsr7Response($e->getResponse());
  182. }
  183. return $error;
  184. }
  185. }