[HttpClient] fix reading the body after a ClientException
This commit is contained in:
parent
6000e51629
commit
0159a39709
40
src/Symfony/Component/HttpClient/Internal/Canary.php
Normal file
40
src/Symfony/Component/HttpClient/Internal/Canary.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?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\Internal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Nicolas Grekas <p@tchwork.com>
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Canary
|
||||||
|
{
|
||||||
|
private $canceller;
|
||||||
|
|
||||||
|
public function __construct(\Closure $canceller)
|
||||||
|
{
|
||||||
|
$this->canceller = $canceller;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cancel()
|
||||||
|
{
|
||||||
|
if (($canceller = $this->canceller) instanceof \Closure) {
|
||||||
|
$this->canceller = null;
|
||||||
|
$canceller();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __destruct()
|
||||||
|
{
|
||||||
|
$this->cancel();
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,7 @@ use Psr\Log\LoggerInterface;
|
|||||||
use Symfony\Component\HttpClient\Chunk\FirstChunk;
|
use Symfony\Component\HttpClient\Chunk\FirstChunk;
|
||||||
use Symfony\Component\HttpClient\Chunk\InformationalChunk;
|
use Symfony\Component\HttpClient\Chunk\InformationalChunk;
|
||||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||||
|
use Symfony\Component\HttpClient\Internal\Canary;
|
||||||
use Symfony\Component\HttpClient\Internal\ClientState;
|
use Symfony\Component\HttpClient\Internal\ClientState;
|
||||||
use Symfony\Component\HttpClient\Internal\CurlClientState;
|
use Symfony\Component\HttpClient\Internal\CurlClientState;
|
||||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||||
@ -149,6 +150,31 @@ final class CurlResponse implements ResponseInterface
|
|||||||
// Schedule the request in a non-blocking way
|
// Schedule the request in a non-blocking way
|
||||||
$multi->openHandles[$id] = [$ch, $options];
|
$multi->openHandles[$id] = [$ch, $options];
|
||||||
curl_multi_add_handle($multi->handle, $ch);
|
curl_multi_add_handle($multi->handle, $ch);
|
||||||
|
|
||||||
|
$this->canary = new Canary(static function () use ($ch, $multi, $id) {
|
||||||
|
unset($multi->openHandles[$id], $multi->handlesActivity[$id]);
|
||||||
|
curl_setopt($ch, \CURLOPT_PRIVATE, '_0');
|
||||||
|
|
||||||
|
if (self::$performing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_multi_remove_handle($multi->handle, $ch);
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
\CURLOPT_NOPROGRESS => true,
|
||||||
|
\CURLOPT_PROGRESSFUNCTION => null,
|
||||||
|
\CURLOPT_HEADERFUNCTION => null,
|
||||||
|
\CURLOPT_WRITEFUNCTION => null,
|
||||||
|
\CURLOPT_READFUNCTION => null,
|
||||||
|
\CURLOPT_INFILE => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!$multi->openHandles) {
|
||||||
|
// Schedule DNS cache eviction for the next request
|
||||||
|
$multi->dnsCache->evictions = $multi->dnsCache->evictions ?: $multi->dnsCache->removals;
|
||||||
|
$multi->dnsCache->removals = $multi->dnsCache->hostnames = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -199,48 +225,11 @@ final class CurlResponse implements ResponseInterface
|
|||||||
|
|
||||||
public function __destruct()
|
public function __destruct()
|
||||||
{
|
{
|
||||||
try {
|
|
||||||
if (null === $this->timeout) {
|
if (null === $this->timeout) {
|
||||||
return; // Unused pushed response
|
return; // Unused pushed response
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->doDestruct();
|
$this->doDestruct();
|
||||||
} finally {
|
|
||||||
$multi = clone $this->multi;
|
|
||||||
|
|
||||||
$this->close();
|
|
||||||
|
|
||||||
if (!$this->multi->openHandles) {
|
|
||||||
// Schedule DNS cache eviction for the next request
|
|
||||||
$this->multi->dnsCache->evictions = $this->multi->dnsCache->evictions ?: $this->multi->dnsCache->removals;
|
|
||||||
$this->multi->dnsCache->removals = $this->multi->dnsCache->hostnames = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->multi = $multi;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
private function close(): void
|
|
||||||
{
|
|
||||||
unset($this->multi->openHandles[$this->id], $this->multi->handlesActivity[$this->id]);
|
|
||||||
curl_setopt($this->handle, \CURLOPT_PRIVATE, '_0');
|
|
||||||
|
|
||||||
if (self::$performing) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
curl_multi_remove_handle($this->multi->handle, $this->handle);
|
|
||||||
curl_setopt_array($this->handle, [
|
|
||||||
\CURLOPT_NOPROGRESS => true,
|
|
||||||
\CURLOPT_PROGRESSFUNCTION => null,
|
|
||||||
\CURLOPT_HEADERFUNCTION => null,
|
|
||||||
\CURLOPT_WRITEFUNCTION => null,
|
|
||||||
\CURLOPT_READFUNCTION => null,
|
|
||||||
\CURLOPT_INFILE => null,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -14,6 +14,7 @@ namespace Symfony\Component\HttpClient\Response;
|
|||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Component\HttpClient\Chunk\FirstChunk;
|
use Symfony\Component\HttpClient\Chunk\FirstChunk;
|
||||||
use Symfony\Component\HttpClient\Exception\TransportException;
|
use Symfony\Component\HttpClient\Exception\TransportException;
|
||||||
|
use Symfony\Component\HttpClient\Internal\Canary;
|
||||||
use Symfony\Component\HttpClient\Internal\ClientState;
|
use Symfony\Component\HttpClient\Internal\ClientState;
|
||||||
use Symfony\Component\HttpClient\Internal\NativeClientState;
|
use Symfony\Component\HttpClient\Internal\NativeClientState;
|
||||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||||
@ -43,7 +44,7 @@ final class NativeResponse implements ResponseInterface
|
|||||||
public function __construct(NativeClientState $multi, $context, string $url, array $options, array &$info, callable $resolveRedirect, ?callable $onProgress, ?LoggerInterface $logger)
|
public function __construct(NativeClientState $multi, $context, string $url, array $options, array &$info, callable $resolveRedirect, ?callable $onProgress, ?LoggerInterface $logger)
|
||||||
{
|
{
|
||||||
$this->multi = $multi;
|
$this->multi = $multi;
|
||||||
$this->id = (int) $context;
|
$this->id = $id = (int) $context;
|
||||||
$this->context = $context;
|
$this->context = $context;
|
||||||
$this->url = $url;
|
$this->url = $url;
|
||||||
$this->logger = $logger;
|
$this->logger = $logger;
|
||||||
@ -63,6 +64,10 @@ final class NativeResponse implements ResponseInterface
|
|||||||
$this->initializer = static function (self $response) {
|
$this->initializer = static function (self $response) {
|
||||||
return null === $response->remaining;
|
return null === $response->remaining;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$this->canary = new Canary(static function () use ($multi, $id) {
|
||||||
|
unset($multi->openHandles[$id], $multi->handlesActivity[$id]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -88,17 +93,11 @@ final class NativeResponse implements ResponseInterface
|
|||||||
try {
|
try {
|
||||||
$this->doDestruct();
|
$this->doDestruct();
|
||||||
} finally {
|
} finally {
|
||||||
$multi = clone $this->multi;
|
|
||||||
|
|
||||||
$this->close();
|
|
||||||
|
|
||||||
// Clear the DNS cache when all requests completed
|
// Clear the DNS cache when all requests completed
|
||||||
if (0 >= --$this->multi->responseCount) {
|
if (0 >= --$this->multi->responseCount) {
|
||||||
$this->multi->responseCount = 0;
|
$this->multi->responseCount = 0;
|
||||||
$this->multi->dnsCache = [];
|
$this->multi->dnsCache = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->multi = $multi;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,8 +191,8 @@ final class NativeResponse implements ResponseInterface
|
|||||||
*/
|
*/
|
||||||
private function close(): void
|
private function close(): void
|
||||||
{
|
{
|
||||||
unset($this->multi->openHandles[$this->id], $this->multi->handlesActivity[$this->id]);
|
$this->canary->cancel();
|
||||||
$this->handle = $this->buffer = $this->onProgress = null;
|
$this->handle = $this->buffer = $this->inflate = $this->onProgress = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -37,6 +37,7 @@ trait ResponseTrait
|
|||||||
{
|
{
|
||||||
private $logger;
|
private $logger;
|
||||||
private $headers = [];
|
private $headers = [];
|
||||||
|
private $canary;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var callable|null A callback that initializes the two previous properties
|
* @var callable|null A callback that initializes the two previous properties
|
||||||
@ -207,7 +208,11 @@ trait ResponseTrait
|
|||||||
/**
|
/**
|
||||||
* Closes the response and all its network handles.
|
* Closes the response and all its network handles.
|
||||||
*/
|
*/
|
||||||
abstract protected function close(): void;
|
private function close(): void
|
||||||
|
{
|
||||||
|
$this->canary->cancel();
|
||||||
|
$this->inflate = null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds pending responses to the activity list.
|
* Adds pending responses to the activity list.
|
||||||
|
@ -134,6 +134,7 @@ abstract class HttpClientTestCase extends BaseHttpClientTestCase
|
|||||||
// The response content-type mustn't be json as that calls getContent
|
// The response content-type mustn't be json as that calls getContent
|
||||||
// @see src/Symfony/Component/HttpClient/Exception/HttpExceptionTrait.php:58
|
// @see src/Symfony/Component/HttpClient/Exception/HttpExceptionTrait.php:58
|
||||||
$this->assertStringNotContainsString('json', $e->getResponse()->getHeaders(false)['content-type'][0] ?? '');
|
$this->assertStringNotContainsString('json', $e->getResponse()->getHeaders(false)['content-type'][0] ?? '');
|
||||||
|
unset($e);
|
||||||
|
|
||||||
$r = new \ReflectionProperty($client, 'multi');
|
$r = new \ReflectionProperty($client, 'multi');
|
||||||
$r->setAccessible(true);
|
$r->setAccessible(true);
|
||||||
|
@ -66,6 +66,9 @@ switch ($vars['REQUEST_URI']) {
|
|||||||
case '/404-gzipped':
|
case '/404-gzipped':
|
||||||
header('Content-Type: text/plain', true, 404);
|
header('Content-Type: text/plain', true, 404);
|
||||||
ob_start('ob_gzhandler');
|
ob_start('ob_gzhandler');
|
||||||
|
@ob_flush();
|
||||||
|
flush();
|
||||||
|
usleep(300000);
|
||||||
echo 'some text';
|
echo 'some text';
|
||||||
exit;
|
exit;
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user