2019-01-27 20:00:39 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
/*
|
|
|
|
* This file is part of the Symfony package.
|
|
|
|
*
|
|
|
|
* (c) Fabien Potencier <fabien@symfony.com>
|
|
|
|
*
|
|
|
|
* For the full copyright and license information, please view the LICENSE
|
|
|
|
* file that was distributed with this source code.
|
|
|
|
*/
|
|
|
|
|
|
|
|
namespace Symfony\Component\HttpClient\Response;
|
|
|
|
|
|
|
|
use Symfony\Component\HttpClient\Chunk\DataChunk;
|
|
|
|
use Symfony\Component\HttpClient\Chunk\ErrorChunk;
|
2019-03-26 00:54:03 +00:00
|
|
|
use Symfony\Component\HttpClient\Chunk\FirstChunk;
|
2019-01-27 20:00:39 +00:00
|
|
|
use Symfony\Component\HttpClient\Chunk\LastChunk;
|
|
|
|
use Symfony\Component\HttpClient\Exception\ClientException;
|
2019-03-09 16:12:38 +00:00
|
|
|
use Symfony\Component\HttpClient\Exception\JsonException;
|
2019-01-27 20:00:39 +00:00
|
|
|
use Symfony\Component\HttpClient\Exception\RedirectionException;
|
|
|
|
use Symfony\Component\HttpClient\Exception\ServerException;
|
|
|
|
use Symfony\Component\HttpClient\Exception\TransportException;
|
2019-04-07 10:02:11 +01:00
|
|
|
use Symfony\Component\HttpClient\Internal\ClientState;
|
2019-01-27 20:00:39 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Implements the common logic for response classes.
|
|
|
|
*
|
|
|
|
* @author Nicolas Grekas <p@tchwork.com>
|
|
|
|
*
|
|
|
|
* @internal
|
|
|
|
*/
|
|
|
|
trait ResponseTrait
|
|
|
|
{
|
2019-04-04 15:02:57 +01:00
|
|
|
private $logger;
|
2019-01-27 20:00:39 +00:00
|
|
|
private $headers = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var callable|null A callback that initializes the two previous properties
|
|
|
|
*/
|
|
|
|
private $initializer;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var resource A php://temp stream typically
|
|
|
|
*/
|
|
|
|
private $content;
|
|
|
|
|
|
|
|
private $info = [
|
2019-04-02 11:03:40 +01:00
|
|
|
'response_headers' => [],
|
2019-03-08 13:46:03 +00:00
|
|
|
'http_code' => 0,
|
2019-01-27 20:00:39 +00:00
|
|
|
'error' => null,
|
|
|
|
];
|
|
|
|
|
2019-04-07 10:02:11 +01:00
|
|
|
/** @var resource */
|
2019-01-27 20:00:39 +00:00
|
|
|
private $handle;
|
|
|
|
private $id;
|
|
|
|
private $timeout;
|
|
|
|
private $finalInfo;
|
|
|
|
private $offset = 0;
|
2019-03-09 16:12:38 +00:00
|
|
|
private $jsonData;
|
2019-01-27 20:00:39 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* {@inheritdoc}
|
|
|
|
*/
|
|
|
|
public function getStatusCode(): int
|
|
|
|
{
|
|
|
|
if ($this->initializer) {
|
|
|
|
($this->initializer)($this);
|
|
|
|
$this->initializer = null;
|
|
|
|
}
|
|
|
|
|
2019-03-08 13:46:03 +00:00
|
|
|
return $this->info['http_code'];
|
2019-01-27 20:00:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* {@inheritdoc}
|
|
|
|
*/
|
|
|
|
public function getHeaders(bool $throw = true): array
|
|
|
|
{
|
|
|
|
if ($this->initializer) {
|
|
|
|
($this->initializer)($this);
|
|
|
|
$this->initializer = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($throw) {
|
|
|
|
$this->checkStatusCode();
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->headers;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* {@inheritdoc}
|
|
|
|
*/
|
|
|
|
public function getContent(bool $throw = true): string
|
|
|
|
{
|
|
|
|
if ($this->initializer) {
|
|
|
|
($this->initializer)($this);
|
|
|
|
$this->initializer = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($throw) {
|
|
|
|
$this->checkStatusCode();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (null === $this->content) {
|
2019-03-11 09:55:59 +00:00
|
|
|
$content = null;
|
2019-01-27 20:00:39 +00:00
|
|
|
|
|
|
|
foreach (self::stream([$this]) as $chunk) {
|
2019-03-11 09:55:59 +00:00
|
|
|
if (!$chunk->isLast()) {
|
|
|
|
$content .= $chunk->getContent();
|
|
|
|
}
|
2019-01-27 20:00:39 +00:00
|
|
|
}
|
|
|
|
|
2019-10-30 11:53:18 +00:00
|
|
|
if (null !== $content) {
|
|
|
|
return $content;
|
2019-01-27 20:00:39 +00:00
|
|
|
}
|
|
|
|
|
2019-10-30 11:53:18 +00:00
|
|
|
if ('HEAD' === $this->info['http_method'] || \in_array($this->info['http_code'], [204, 304], true)) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new TransportException('Cannot get the content of the response twice: the request was issued with option "buffer" set to false.');
|
2019-01-27 20:00:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
foreach (self::stream([$this]) as $chunk) {
|
|
|
|
// Chunks are buffered in $this->content already
|
|
|
|
}
|
|
|
|
|
|
|
|
rewind($this->content);
|
|
|
|
|
|
|
|
return stream_get_contents($this->content);
|
|
|
|
}
|
|
|
|
|
2019-03-09 16:12:38 +00:00
|
|
|
/**
|
|
|
|
* {@inheritdoc}
|
|
|
|
*/
|
|
|
|
public function toArray(bool $throw = true): array
|
|
|
|
{
|
|
|
|
if ('' === $content = $this->getContent($throw)) {
|
|
|
|
throw new TransportException('Response body is empty.');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (null !== $this->jsonData) {
|
|
|
|
return $this->jsonData;
|
|
|
|
}
|
|
|
|
|
|
|
|
$contentType = $this->headers['content-type'][0] ?? 'application/json';
|
|
|
|
|
|
|
|
if (!preg_match('/\bjson\b/i', $contentType)) {
|
|
|
|
throw new JsonException(sprintf('Response content-type is "%s" while a JSON-compatible one was expected.', $contentType));
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2019-06-05 12:58:47 +01:00
|
|
|
$content = json_decode($content, true, 512, JSON_BIGINT_AS_STRING | (\PHP_VERSION_ID >= 70300 ? \JSON_THROW_ON_ERROR : 0));
|
2019-03-09 16:12:38 +00:00
|
|
|
} catch (\JsonException $e) {
|
|
|
|
throw new JsonException($e->getMessage(), $e->getCode());
|
|
|
|
}
|
|
|
|
|
|
|
|
if (\PHP_VERSION_ID < 70300 && JSON_ERROR_NONE !== json_last_error()) {
|
|
|
|
throw new JsonException(json_last_error_msg(), json_last_error());
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!\is_array($content)) {
|
|
|
|
throw new JsonException(sprintf('JSON content was expected to decode to an array, %s returned.', \gettype($content)));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (null !== $this->content) {
|
|
|
|
// Option "buffer" is true
|
|
|
|
return $this->jsonData = $content;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $content;
|
|
|
|
}
|
|
|
|
|
2019-06-03 18:56:51 +01:00
|
|
|
/**
|
|
|
|
* {@inheritdoc}
|
|
|
|
*/
|
|
|
|
public function cancel(): void
|
|
|
|
{
|
|
|
|
$this->info['error'] = 'Response has been canceled.';
|
|
|
|
$this->close();
|
|
|
|
}
|
|
|
|
|
2019-01-27 20:00:39 +00:00
|
|
|
/**
|
|
|
|
* Closes the response and all its network handles.
|
|
|
|
*/
|
|
|
|
abstract protected function close(): void;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adds pending responses to the activity list.
|
|
|
|
*/
|
|
|
|
abstract protected static function schedule(self $response, array &$runningResponses): void;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Performs all pending non-blocking operations.
|
|
|
|
*/
|
2019-04-07 10:02:11 +01:00
|
|
|
abstract protected static function perform(ClientState $multi, array &$responses): void;
|
2019-01-27 20:00:39 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Waits for network activity.
|
|
|
|
*/
|
2019-04-07 10:02:11 +01:00
|
|
|
abstract protected static function select(ClientState $multi, float $timeout): int;
|
2019-01-27 20:00:39 +00:00
|
|
|
|
2019-05-27 19:15:39 +01:00
|
|
|
private static function addResponseHeaders(array $responseHeaders, array &$info, array &$headers, string &$debug = ''): void
|
2019-01-27 20:00:39 +00:00
|
|
|
{
|
2019-04-02 11:03:40 +01:00
|
|
|
foreach ($responseHeaders as $h) {
|
2019-01-27 20:00:39 +00:00
|
|
|
if (11 <= \strlen($h) && '/' === $h[4] && preg_match('#^HTTP/\d+(?:\.\d+)? ([12345]\d\d) .*#', $h, $m)) {
|
2019-05-27 19:15:39 +01:00
|
|
|
if ($headers) {
|
|
|
|
$debug .= "< \r\n";
|
|
|
|
$headers = [];
|
|
|
|
}
|
2019-03-08 13:46:03 +00:00
|
|
|
$info['http_code'] = (int) $m[1];
|
2019-01-27 20:00:39 +00:00
|
|
|
} elseif (2 === \count($m = explode(':', $h, 2))) {
|
2019-03-08 13:46:03 +00:00
|
|
|
$headers[strtolower($m[0])][] = ltrim($m[1]);
|
2019-01-27 20:00:39 +00:00
|
|
|
}
|
|
|
|
|
2019-05-27 19:15:39 +01:00
|
|
|
$debug .= "< {$h}\r\n";
|
2019-04-02 11:03:40 +01:00
|
|
|
$info['response_headers'][] = $h;
|
2019-01-27 20:00:39 +00:00
|
|
|
}
|
|
|
|
|
2019-05-27 19:15:39 +01:00
|
|
|
$debug .= "< \r\n";
|
|
|
|
|
2019-03-08 13:46:03 +00:00
|
|
|
if (!$info['http_code']) {
|
2019-01-27 20:00:39 +00:00
|
|
|
throw new TransportException('Invalid or missing HTTP status line.');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private function checkStatusCode()
|
|
|
|
{
|
2019-03-08 13:46:03 +00:00
|
|
|
if (500 <= $this->info['http_code']) {
|
2019-01-27 20:00:39 +00:00
|
|
|
throw new ServerException($this);
|
|
|
|
}
|
|
|
|
|
2019-03-08 13:46:03 +00:00
|
|
|
if (400 <= $this->info['http_code']) {
|
2019-01-27 20:00:39 +00:00
|
|
|
throw new ClientException($this);
|
|
|
|
}
|
|
|
|
|
2019-03-08 13:46:03 +00:00
|
|
|
if (300 <= $this->info['http_code']) {
|
2019-01-27 20:00:39 +00:00
|
|
|
throw new RedirectionException($this);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Ensures the request is always sent and that the response code was checked.
|
|
|
|
*/
|
|
|
|
private function doDestruct()
|
|
|
|
{
|
|
|
|
if ($this->initializer && null === $this->info['error']) {
|
|
|
|
($this->initializer)($this);
|
|
|
|
$this->initializer = null;
|
|
|
|
$this->checkStatusCode();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Implements an event loop based on a buffer activity queue.
|
|
|
|
*
|
|
|
|
* @internal
|
|
|
|
*/
|
|
|
|
public static function stream(iterable $responses, float $timeout = null): \Generator
|
|
|
|
{
|
|
|
|
$runningResponses = [];
|
|
|
|
|
|
|
|
foreach ($responses as $response) {
|
|
|
|
self::schedule($response, $runningResponses);
|
|
|
|
}
|
|
|
|
|
|
|
|
$lastActivity = microtime(true);
|
|
|
|
$isTimeout = false;
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
$hasActivity = false;
|
|
|
|
$timeoutMax = 0;
|
|
|
|
$timeoutMin = $timeout ?? INF;
|
|
|
|
|
2019-04-07 10:02:11 +01:00
|
|
|
/** @var ClientState $multi */
|
2019-01-27 20:00:39 +00:00
|
|
|
foreach ($runningResponses as $i => [$multi]) {
|
|
|
|
$responses = &$runningResponses[$i][1];
|
|
|
|
self::perform($multi, $responses);
|
|
|
|
|
|
|
|
foreach ($responses as $j => $response) {
|
|
|
|
$timeoutMax = $timeout ?? max($timeoutMax, $response->timeout);
|
|
|
|
$timeoutMin = min($timeoutMin, $response->timeout, 1);
|
2019-03-19 09:44:44 +00:00
|
|
|
$chunk = false;
|
2019-01-27 20:00:39 +00:00
|
|
|
|
|
|
|
if (isset($multi->handlesActivity[$j])) {
|
|
|
|
// no-op
|
|
|
|
} elseif (!isset($multi->openHandles[$j])) {
|
|
|
|
unset($responses[$j]);
|
|
|
|
continue;
|
|
|
|
} elseif ($isTimeout) {
|
2019-10-31 09:21:58 +00:00
|
|
|
$multi->handlesActivity[$j] = [new ErrorChunk($response->offset, sprintf('Idle timeout reached for "%s".', $response->getInfo('url')))];
|
2019-01-27 20:00:39 +00:00
|
|
|
} else {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
while ($multi->handlesActivity[$j] ?? false) {
|
|
|
|
$hasActivity = true;
|
|
|
|
$isTimeout = false;
|
|
|
|
|
|
|
|
if (\is_string($chunk = array_shift($multi->handlesActivity[$j]))) {
|
|
|
|
$response->offset += \strlen($chunk);
|
|
|
|
$chunk = new DataChunk($response->offset, $chunk);
|
|
|
|
} elseif (null === $chunk) {
|
2019-03-11 09:55:59 +00:00
|
|
|
$e = $multi->handlesActivity[$j][0];
|
|
|
|
unset($responses[$j], $multi->handlesActivity[$j]);
|
|
|
|
$response->close();
|
|
|
|
|
|
|
|
if (null !== $e) {
|
2019-01-27 20:00:39 +00:00
|
|
|
$response->info['error'] = $e->getMessage();
|
|
|
|
|
|
|
|
if ($e instanceof \Error) {
|
|
|
|
throw $e;
|
|
|
|
}
|
|
|
|
|
2019-03-19 09:44:44 +00:00
|
|
|
$chunk = new ErrorChunk($response->offset, $e);
|
2019-01-27 20:00:39 +00:00
|
|
|
} else {
|
|
|
|
$chunk = new LastChunk($response->offset);
|
|
|
|
}
|
|
|
|
} elseif ($chunk instanceof ErrorChunk) {
|
|
|
|
unset($responses[$j]);
|
|
|
|
$isTimeout = true;
|
2019-09-19 19:45:47 +01:00
|
|
|
} elseif ($chunk instanceof FirstChunk) {
|
|
|
|
if ($response->logger) {
|
|
|
|
$info = $response->getInfo();
|
|
|
|
$response->logger->info(sprintf('Response: "%s %s"', $info['http_code'], $info['url']));
|
|
|
|
}
|
|
|
|
|
|
|
|
yield $response => $chunk;
|
|
|
|
|
|
|
|
if ($response->initializer && null === $response->info['error']) {
|
|
|
|
// Ensure the HTTP status code is always checked
|
|
|
|
$response->getHeaders(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
continue;
|
2019-01-27 20:00:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
yield $response => $chunk;
|
|
|
|
}
|
|
|
|
|
|
|
|
unset($multi->handlesActivity[$j]);
|
|
|
|
|
2019-09-19 19:45:47 +01:00
|
|
|
if ($chunk instanceof ErrorChunk && !$chunk->didThrow()) {
|
2019-01-27 20:00:39 +00:00
|
|
|
// Ensure transport exceptions are always thrown
|
|
|
|
$chunk->getContent();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$responses) {
|
|
|
|
unset($runningResponses[$i]);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Prevent memory leaks
|
|
|
|
$multi->handlesActivity = $multi->handlesActivity ?: [];
|
|
|
|
$multi->openHandles = $multi->openHandles ?: [];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$runningResponses) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($hasActivity) {
|
|
|
|
$lastActivity = microtime(true);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (self::select($multi, $timeoutMin)) {
|
|
|
|
case -1: usleep(min(500, 1E6 * $timeoutMin)); break;
|
|
|
|
case 0: $isTimeout = microtime(true) - $lastActivity > $timeoutMax; break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|