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

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