vendor/symfony/http-client/Response/ResponseTrait.php line 291

Open in your IDE?
  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 Symfony\Component\HttpClient\Chunk\DataChunk;
  12. use Symfony\Component\HttpClient\Chunk\ErrorChunk;
  13. use Symfony\Component\HttpClient\Chunk\FirstChunk;
  14. use Symfony\Component\HttpClient\Chunk\LastChunk;
  15. use Symfony\Component\HttpClient\Exception\ClientException;
  16. use Symfony\Component\HttpClient\Exception\JsonException;
  17. use Symfony\Component\HttpClient\Exception\RedirectionException;
  18. use Symfony\Component\HttpClient\Exception\ServerException;
  19. use Symfony\Component\HttpClient\Exception\TransportException;
  20. use Symfony\Component\HttpClient\Internal\ClientState;
  21. use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
  22. use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
  23. use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
  24. use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
  25. /**
  26.  * Implements the common logic for response classes.
  27.  *
  28.  * @author Nicolas Grekas <p@tchwork.com>
  29.  *
  30.  * @internal
  31.  */
  32. trait ResponseTrait
  33. {
  34.     private $logger;
  35.     private $headers = [];
  36.     private $canary;
  37.     /**
  38.      * @var callable|null A callback that initializes the two previous properties
  39.      */
  40.     private $initializer;
  41.     private $info = [
  42.         'response_headers' => [],
  43.         'http_code' => 0,
  44.         'error' => null,
  45.         'canceled' => false,
  46.     ];
  47.     /** @var object|resource */
  48.     private $handle;
  49.     private $id;
  50.     private $timeout 0;
  51.     private $inflate;
  52.     private $shouldBuffer;
  53.     private $content;
  54.     private $finalInfo;
  55.     private $offset 0;
  56.     private $jsonData;
  57.     /**
  58.      * {@inheritdoc}
  59.      */
  60.     public function getStatusCode(): int
  61.     {
  62.         if ($this->initializer) {
  63.             self::initialize($this);
  64.         }
  65.         return $this->info['http_code'];
  66.     }
  67.     /**
  68.      * {@inheritdoc}
  69.      */
  70.     public function getHeaders(bool $throw true): array
  71.     {
  72.         if ($this->initializer) {
  73.             self::initialize($this);
  74.         }
  75.         if ($throw) {
  76.             $this->checkStatusCode();
  77.         }
  78.         return $this->headers;
  79.     }
  80.     /**
  81.      * {@inheritdoc}
  82.      */
  83.     public function getContent(bool $throw true): string
  84.     {
  85.         if ($this->initializer) {
  86.             self::initialize($this);
  87.         }
  88.         if ($throw) {
  89.             $this->checkStatusCode();
  90.         }
  91.         if (null === $this->content) {
  92.             $content null;
  93.             foreach (self::stream([$this]) as $chunk) {
  94.                 if (!$chunk->isLast()) {
  95.                     $content .= $chunk->getContent();
  96.                 }
  97.             }
  98.             if (null !== $content) {
  99.                 return $content;
  100.             }
  101.             if (null === $this->content) {
  102.                 throw new TransportException('Cannot get the content of the response twice: buffering is disabled.');
  103.             }
  104.         } else {
  105.             foreach (self::stream([$this]) as $chunk) {
  106.                 // Chunks are buffered in $this->content already
  107.             }
  108.         }
  109.         rewind($this->content);
  110.         return stream_get_contents($this->content);
  111.     }
  112.     /**
  113.      * {@inheritdoc}
  114.      */
  115.     public function toArray(bool $throw true): array
  116.     {
  117.         if ('' === $content $this->getContent($throw)) {
  118.             throw new JsonException('Response body is empty.');
  119.         }
  120.         if (null !== $this->jsonData) {
  121.             return $this->jsonData;
  122.         }
  123.         try {
  124.             $content json_decode($contenttrue512, \JSON_BIGINT_AS_STRING | (\PHP_VERSION_ID >= 70300 ? \JSON_THROW_ON_ERROR 0));
  125.         } catch (\JsonException $e) {
  126.             throw new JsonException($e->getMessage().sprintf(' for "%s".'$this->getInfo('url')), $e->getCode());
  127.         }
  128.         if (\PHP_VERSION_ID 70300 && \JSON_ERROR_NONE !== json_last_error()) {
  129.             throw new JsonException(json_last_error_msg().sprintf(' for "%s".'$this->getInfo('url')), json_last_error());
  130.         }
  131.         if (!\is_array($content)) {
  132.             throw new JsonException(sprintf('JSON content was expected to decode to an array, "%s" returned for "%s".'get_debug_type($content), $this->getInfo('url')));
  133.         }
  134.         if (null !== $this->content) {
  135.             // Option "buffer" is true
  136.             return $this->jsonData $content;
  137.         }
  138.         return $content;
  139.     }
  140.     /**
  141.      * {@inheritdoc}
  142.      */
  143.     public function cancel(): void
  144.     {
  145.         $this->info['canceled'] = true;
  146.         $this->info['error'] = 'Response has been canceled.';
  147.         $this->close();
  148.     }
  149.     /**
  150.      * Casts the response to a PHP stream resource.
  151.      *
  152.      * @return resource
  153.      *
  154.      * @throws TransportExceptionInterface   When a network error occurs
  155.      * @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
  156.      * @throws ClientExceptionInterface      On a 4xx when $throw is true
  157.      * @throws ServerExceptionInterface      On a 5xx when $throw is true
  158.      */
  159.     public function toStream(bool $throw true)
  160.     {
  161.         if ($throw) {
  162.             // Ensure headers arrived
  163.             $this->getHeaders($throw);
  164.         }
  165.         $stream StreamWrapper::createResource($this);
  166.         stream_get_meta_data($stream)['wrapper_data']
  167.             ->bindHandles($this->handle$this->content);
  168.         return $stream;
  169.     }
  170.     public function __sleep()
  171.     {
  172.         throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
  173.     }
  174.     public function __wakeup()
  175.     {
  176.         throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
  177.     }
  178.     /**
  179.      * Closes the response and all its network handles.
  180.      */
  181.     private function close(): void
  182.     {
  183.         $this->canary->cancel();
  184.         $this->inflate null;
  185.     }
  186.     /**
  187.      * Adds pending responses to the activity list.
  188.      */
  189.     abstract protected static function schedule(self $response, array &$runningResponses): void;
  190.     /**
  191.      * Performs all pending non-blocking operations.
  192.      */
  193.     abstract protected static function perform(ClientState $multi, array &$responses): void;
  194.     /**
  195.      * Waits for network activity.
  196.      */
  197.     abstract protected static function select(ClientState $multifloat $timeout): int;
  198.     private static function initialize(self $response): void
  199.     {
  200.         if (null !== $response->info['error']) {
  201.             throw new TransportException($response->info['error']);
  202.         }
  203.         try {
  204.             if (($response->initializer)($response)) {
  205.                 foreach (self::stream([$response]) as $chunk) {
  206.                     if ($chunk->isFirst()) {
  207.                         break;
  208.                     }
  209.                 }
  210.             }
  211.         } catch (\Throwable $e) {
  212.             // Persist timeouts thrown during initialization
  213.             $response->info['error'] = $e->getMessage();
  214.             $response->close();
  215.             throw $e;
  216.         }
  217.         $response->initializer null;
  218.     }
  219.     private static function addResponseHeaders(array $responseHeaders, array &$info, array &$headersstring &$debug ''): void
  220.     {
  221.         foreach ($responseHeaders as $h) {
  222.             if (11 <= \strlen($h) && '/' === $h[4] && preg_match('#^HTTP/\d+(?:\.\d+)? ([1-9]\d\d)(?: |$)#'$h$m)) {
  223.                 if ($headers) {
  224.                     $debug .= "< \r\n";
  225.                     $headers = [];
  226.                 }
  227.                 $info['http_code'] = (int) $m[1];
  228.             } elseif (=== \count($m explode(':'$h2))) {
  229.                 $headers[strtolower($m[0])][] = ltrim($m[1]);
  230.             }
  231.             $debug .= "< {$h}\r\n";
  232.             $info['response_headers'][] = $h;
  233.         }
  234.         $debug .= "< \r\n";
  235.         if (!$info['http_code']) {
  236.             throw new TransportException(sprintf('Invalid or missing HTTP status line for "%s".'implode(''$info['url'])));
  237.         }
  238.     }
  239.     private function checkStatusCode()
  240.     {
  241.         if (500 <= $this->info['http_code']) {
  242.             throw new ServerException($this);
  243.         }
  244.         if (400 <= $this->info['http_code']) {
  245.             throw new ClientException($this);
  246.         }
  247.         if (300 <= $this->info['http_code']) {
  248.             throw new RedirectionException($this);
  249.         }
  250.     }
  251.     /**
  252.      * Ensures the request is always sent and that the response code was checked.
  253.      */
  254.     private function doDestruct()
  255.     {
  256.         $this->shouldBuffer true;
  257.         if ($this->initializer && null === $this->info['error']) {
  258.             self::initialize($this);
  259.             $this->checkStatusCode();
  260.         }
  261.     }
  262.     /**
  263.      * Implements an event loop based on a buffer activity queue.
  264.      *
  265.      * @internal
  266.      */
  267.     public static function stream(iterable $responsesfloat $timeout null): \Generator
  268.     {
  269.         $runningResponses = [];
  270.         foreach ($responses as $response) {
  271.             self::schedule($response$runningResponses);
  272.         }
  273.         $lastActivity microtime(true);
  274.         $elapsedTimeout 0;
  275.         while (true) {
  276.             $hasActivity false;
  277.             $timeoutMax 0;
  278.             $timeoutMin $timeout ?? \INF;
  279.             /** @var ClientState $multi */
  280.             foreach ($runningResponses as $i => [$multi]) {
  281.                 $responses = &$runningResponses[$i][1];
  282.                 self::perform($multi$responses);
  283.                 foreach ($responses as $j => $response) {
  284.                     $timeoutMax $timeout ?? max($timeoutMax$response->timeout);
  285.                     $timeoutMin min($timeoutMin$response->timeout1);
  286.                     $chunk false;
  287.                     if (isset($multi->handlesActivity[$j])) {
  288.                         // no-op
  289.                     } elseif (!isset($multi->openHandles[$j])) {
  290.                         unset($responses[$j]);
  291.                         continue;
  292.                     } elseif ($elapsedTimeout >= $timeoutMax) {
  293.                         $multi->handlesActivity[$j] = [new ErrorChunk($response->offsetsprintf('Idle timeout reached for "%s".'$response->getInfo('url')))];
  294.                     } else {
  295.                         continue;
  296.                     }
  297.                     while ($multi->handlesActivity[$j] ?? false) {
  298.                         $hasActivity true;
  299.                         $elapsedTimeout 0;
  300.                         if (\is_string($chunk array_shift($multi->handlesActivity[$j]))) {
  301.                             if (null !== $response->inflate && false === $chunk = @inflate_add($response->inflate$chunk)) {
  302.                                 $multi->handlesActivity[$j] = [null, new TransportException(sprintf('Error while processing content unencoding for "%s".'$response->getInfo('url')))];
  303.                                 continue;
  304.                             }
  305.                             if ('' !== $chunk && null !== $response->content && \strlen($chunk) !== fwrite($response->content$chunk)) {
  306.                                 $multi->handlesActivity[$j] = [null, new TransportException(sprintf('Failed writing %d bytes to the response buffer.', \strlen($chunk)))];
  307.                                 continue;
  308.                             }
  309.                             $chunkLen = \strlen($chunk);
  310.                             $chunk = new DataChunk($response->offset$chunk);
  311.                             $response->offset += $chunkLen;
  312.                         } elseif (null === $chunk) {
  313.                             $e $multi->handlesActivity[$j][0];
  314.                             unset($responses[$j], $multi->handlesActivity[$j]);
  315.                             $response->close();
  316.                             if (null !== $e) {
  317.                                 $response->info['error'] = $e->getMessage();
  318.                                 if ($e instanceof \Error) {
  319.                                     throw $e;
  320.                                 }
  321.                                 $chunk = new ErrorChunk($response->offset$e);
  322.                             } else {
  323.                                 if (=== $response->offset && null === $response->content) {
  324.                                     $response->content fopen('php://memory''w+');
  325.                                 }
  326.                                 $chunk = new LastChunk($response->offset);
  327.                             }
  328.                         } elseif ($chunk instanceof ErrorChunk) {
  329.                             unset($responses[$j]);
  330.                             $elapsedTimeout $timeoutMax;
  331.                         } elseif ($chunk instanceof FirstChunk) {
  332.                             if ($response->logger) {
  333.                                 $info $response->getInfo();
  334.                                 $response->logger->info(sprintf('Response: "%s %s"'$info['http_code'], $info['url']));
  335.                             }
  336.                             $response->inflate = \extension_loaded('zlib') && $response->inflate && 'gzip' === ($response->headers['content-encoding'][0] ?? null) ? inflate_init(\ZLIB_ENCODING_GZIP) : null;
  337.                             if ($response->shouldBuffer instanceof \Closure) {
  338.                                 try {
  339.                                     $response->shouldBuffer = ($response->shouldBuffer)($response->headers);
  340.                                     if (null !== $response->info['error']) {
  341.                                         throw new TransportException($response->info['error']);
  342.                                     }
  343.                                 } catch (\Throwable $e) {
  344.                                     $response->close();
  345.                                     $multi->handlesActivity[$j] = [null$e];
  346.                                 }
  347.                             }
  348.                             if (true === $response->shouldBuffer) {
  349.                                 $response->content fopen('php://temp''w+');
  350.                             } elseif (\is_resource($response->shouldBuffer)) {
  351.                                 $response->content $response->shouldBuffer;
  352.                             }
  353.                             $response->shouldBuffer null;
  354.                             yield $response => $chunk;
  355.                             if ($response->initializer && null === $response->info['error']) {
  356.                                 // Ensure the HTTP status code is always checked
  357.                                 $response->getHeaders(true);
  358.                             }
  359.                             continue;
  360.                         }
  361.                         yield $response => $chunk;
  362.                     }
  363.                     unset($multi->handlesActivity[$j]);
  364.                     if ($chunk instanceof ErrorChunk && !$chunk->didThrow()) {
  365.                         // Ensure transport exceptions are always thrown
  366.                         $chunk->getContent();
  367.                     }
  368.                 }
  369.                 if (!$responses) {
  370.                     unset($runningResponses[$i]);
  371.                 }
  372.                 // Prevent memory leaks
  373.                 $multi->handlesActivity $multi->handlesActivity ?: [];
  374.                 $multi->openHandles $multi->openHandles ?: [];
  375.             }
  376.             if (!$runningResponses) {
  377.                 break;
  378.             }
  379.             if ($hasActivity) {
  380.                 $lastActivity microtime(true);
  381.                 continue;
  382.             }
  383.             if (-=== self::select($multimin($timeoutMin$timeoutMax $elapsedTimeout))) {
  384.                 usleep(min(5001E6 $timeoutMin));
  385.             }
  386.             $elapsedTimeout microtime(true) - $lastActivity;
  387.         }
  388.     }
  389. }