TransportResponseTrait.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\HttpClient\Response;
  11. use Psr\Log\LoggerInterface;
  12. use Symfony\Component\HttpClient\Chunk\DataChunk;
  13. use Symfony\Component\HttpClient\Chunk\ErrorChunk;
  14. use Symfony\Component\HttpClient\Chunk\FirstChunk;
  15. use Symfony\Component\HttpClient\Chunk\LastChunk;
  16. use Symfony\Component\HttpClient\Exception\TransportException;
  17. use Symfony\Component\HttpClient\Internal\Canary;
  18. use Symfony\Component\HttpClient\Internal\ClientState;
  19. /**
  20. * Implements common logic for transport-level response classes.
  21. *
  22. * @author Nicolas Grekas <p@tchwork.com>
  23. *
  24. * @internal
  25. */
  26. trait TransportResponseTrait
  27. {
  28. /**
  29. * @var \Symfony\Component\HttpClient\Internal\Canary
  30. */
  31. private $canary;
  32. /**
  33. * @var mixed[]
  34. */
  35. private $headers = [];
  36. /**
  37. * @var mixed[]
  38. */
  39. private $info = [
  40. 'response_headers' => [],
  41. 'http_code' => 0,
  42. 'error' => null,
  43. 'canceled' => false,
  44. ];
  45. /** @var object|resource */
  46. private $handle;
  47. /**
  48. * @var int|string
  49. */
  50. private $id;
  51. /**
  52. * @var float|null
  53. */
  54. private $timeout = 0;
  55. /**
  56. * @var \InflateContext|bool|null
  57. */
  58. private $inflate = null;
  59. /**
  60. * @var mixed[]|null
  61. */
  62. private $finalInfo;
  63. /**
  64. * @var \Psr\Log\LoggerInterface|null
  65. */
  66. private $logger;
  67. public function getStatusCode()
  68. {
  69. if ($this->initializer) {
  70. self::initialize($this);
  71. }
  72. return $this->info['http_code'];
  73. }
  74. /**
  75. * @param bool $throw
  76. */
  77. public function getHeaders($throw = true)
  78. {
  79. if ($this->initializer) {
  80. self::initialize($this);
  81. }
  82. if ($throw) {
  83. $this->checkStatusCode();
  84. }
  85. return $this->headers;
  86. }
  87. public function cancel()
  88. {
  89. $this->info['canceled'] = true;
  90. $this->info['error'] = 'Response has been canceled.';
  91. $this->close();
  92. }
  93. /**
  94. * Closes the response and all its network handles.
  95. */
  96. protected function close()
  97. {
  98. $this->canary->cancel();
  99. $this->inflate = null;
  100. }
  101. /**
  102. * Adds pending responses to the activity list.
  103. * @param $this $response
  104. * @param mixed[] $runningResponses
  105. */
  106. abstract protected static function schedule($response, &$runningResponses);
  107. /**
  108. * Performs all pending non-blocking operations.
  109. * @param \Symfony\Component\HttpClient\Internal\ClientState $multi
  110. * @param mixed[] $responses
  111. */
  112. abstract protected static function perform($multi, &$responses);
  113. /**
  114. * Waits for network activity.
  115. * @param \Symfony\Component\HttpClient\Internal\ClientState $multi
  116. * @param float $timeout
  117. */
  118. abstract protected static function select($multi, $timeout);
  119. /**
  120. * @param mixed[] $responseHeaders
  121. * @param mixed[] $info
  122. * @param mixed[] $headers
  123. * @param string $debug
  124. */
  125. private static function addResponseHeaders($responseHeaders, &$info, &$headers, &$debug = '')
  126. {
  127. foreach ($responseHeaders as $h) {
  128. if (11 <= \strlen($h) && '/' === $h[4] && preg_match('#^HTTP/\d+(?:\.\d+)? (\d\d\d)(?: |$)#', $h, $m)) {
  129. if ($headers) {
  130. $debug .= "< \r\n";
  131. $headers = [];
  132. }
  133. $info['http_code'] = (int) $m[1];
  134. } elseif (2 === \count($m = explode(':', $h, 2))) {
  135. $headers[strtolower($m[0])][] = ltrim($m[1]);
  136. }
  137. $debug .= "< {$h}\r\n";
  138. $info['response_headers'][] = $h;
  139. }
  140. $debug .= "< \r\n";
  141. }
  142. /**
  143. * Ensures the request is always sent and that the response code was checked.
  144. */
  145. private function doDestruct()
  146. {
  147. $this->shouldBuffer = true;
  148. if ($this->initializer && null === $this->info['error']) {
  149. self::initialize($this);
  150. $this->checkStatusCode();
  151. }
  152. }
  153. /**
  154. * Implements an event loop based on a buffer activity queue.
  155. *
  156. * @param iterable<array-key, self> $responses
  157. *
  158. * @internal
  159. * @param float|null $timeout
  160. */
  161. public static function stream($responses, $timeout = null)
  162. {
  163. $runningResponses = [];
  164. foreach ($responses as $response) {
  165. self::schedule($response, $runningResponses);
  166. }
  167. $lastActivity = microtime(true);
  168. $elapsedTimeout = 0;
  169. if ($fromLastTimeout = 0.0 === $timeout && '-0' === (string) $timeout) {
  170. $timeout = null;
  171. } elseif ($fromLastTimeout = 0 > $timeout) {
  172. $timeout = -$timeout;
  173. }
  174. while (true) {
  175. $hasActivity = false;
  176. $timeoutMax = 0;
  177. $timeoutMin = $timeout ?? \INF;
  178. /** @var ClientState $multi */
  179. foreach ($runningResponses as $i => [$multi]) {
  180. $responses = &$runningResponses[$i][1];
  181. self::perform($multi, $responses);
  182. foreach ($responses as $j => $response) {
  183. $timeoutMax = $timeout ?? max($timeoutMax, $response->timeout);
  184. $timeoutMin = min($timeoutMin, $response->timeout, 1);
  185. $chunk = false;
  186. if ($fromLastTimeout && null !== $multi->lastTimeout) {
  187. $elapsedTimeout = microtime(true) - $multi->lastTimeout;
  188. }
  189. if (isset($multi->handlesActivity[$j])) {
  190. $multi->lastTimeout = null;
  191. } elseif (!isset($multi->openHandles[$j])) {
  192. unset($responses[$j]);
  193. continue;
  194. } elseif ($elapsedTimeout >= $timeoutMax) {
  195. $multi->handlesActivity[$j] = [new ErrorChunk($response->offset, sprintf('Idle timeout reached for "%s".', $response->getInfo('url')))];
  196. $multi->lastTimeout = $multi->lastTimeout ?? $lastActivity;
  197. } else {
  198. continue;
  199. }
  200. while ($multi->handlesActivity[$j] ?? false) {
  201. $hasActivity = true;
  202. $elapsedTimeout = 0;
  203. if (\is_string($chunk = array_shift($multi->handlesActivity[$j]))) {
  204. if (null !== $response->inflate && false === $chunk = @inflate_add($response->inflate, $chunk)) {
  205. $multi->handlesActivity[$j] = [null, new TransportException(sprintf('Error while processing content unencoding for "%s".', $response->getInfo('url')))];
  206. continue;
  207. }
  208. if ('' !== $chunk && null !== $response->content && \strlen($chunk) !== fwrite($response->content, $chunk)) {
  209. $multi->handlesActivity[$j] = [null, new TransportException(sprintf('Failed writing %d bytes to the response buffer.', \strlen($chunk)))];
  210. continue;
  211. }
  212. $chunkLen = \strlen($chunk);
  213. $chunk = new DataChunk($response->offset, $chunk);
  214. $response->offset += $chunkLen;
  215. } elseif (null === $chunk) {
  216. $e = $multi->handlesActivity[$j][0];
  217. unset($responses[$j], $multi->handlesActivity[$j]);
  218. $response->close();
  219. if (null !== $e) {
  220. $response->info['error'] = $e->getMessage();
  221. if ($e instanceof \Error) {
  222. throw $e;
  223. }
  224. $chunk = new ErrorChunk($response->offset, $e);
  225. } else {
  226. if (0 === $response->offset && null === $response->content) {
  227. $response->content = fopen('php://memory', 'w+');
  228. }
  229. $chunk = new LastChunk($response->offset);
  230. }
  231. } elseif ($chunk instanceof ErrorChunk) {
  232. unset($responses[$j]);
  233. $elapsedTimeout = $timeoutMax;
  234. } elseif ($chunk instanceof FirstChunk) {
  235. if ($response->logger) {
  236. $info = $response->getInfo();
  237. $response->logger->info(sprintf('Response: "%s %s"', $info['http_code'], $info['url']));
  238. }
  239. $response->inflate = \extension_loaded('zlib') && $response->inflate && 'gzip' === ($response->headers['content-encoding'][0] ?? null) ? inflate_init(\ZLIB_ENCODING_GZIP) : null;
  240. if ($response->shouldBuffer instanceof \Closure) {
  241. try {
  242. $response->shouldBuffer = ($response->shouldBuffer)($response->headers);
  243. if (null !== $response->info['error']) {
  244. throw new TransportException($response->info['error']);
  245. }
  246. } catch (\Throwable $e) {
  247. $response->close();
  248. $multi->handlesActivity[$j] = [null, $e];
  249. }
  250. }
  251. if (true === $response->shouldBuffer) {
  252. $response->content = fopen('php://temp', 'w+');
  253. } elseif (\is_resource($response->shouldBuffer)) {
  254. $response->content = $response->shouldBuffer;
  255. }
  256. $response->shouldBuffer = null;
  257. yield $response => $chunk;
  258. if ($response->initializer && null === $response->info['error']) {
  259. // Ensure the HTTP status code is always checked
  260. $response->getHeaders(true);
  261. }
  262. continue;
  263. }
  264. yield $response => $chunk;
  265. }
  266. unset($multi->handlesActivity[$j]);
  267. if ($chunk instanceof ErrorChunk && !$chunk->didThrow()) {
  268. // Ensure transport exceptions are always thrown
  269. $chunk->getContent();
  270. }
  271. }
  272. if (!$responses) {
  273. unset($runningResponses[$i]);
  274. }
  275. // Prevent memory leaks
  276. $multi->handlesActivity = $multi->handlesActivity ?: [];
  277. $multi->openHandles = $multi->openHandles ?: [];
  278. }
  279. if (!$runningResponses) {
  280. break;
  281. }
  282. if ($hasActivity) {
  283. $lastActivity = microtime(true);
  284. continue;
  285. }
  286. if (-1 === self::select($multi, min($timeoutMin, $timeoutMax - $elapsedTimeout))) {
  287. usleep(min(500, 1E6 * $timeoutMin));
  288. }
  289. $elapsedTimeout = microtime(true) - $lastActivity;
  290. }
  291. }
  292. }