[HttpClient] fix reading the body after a ClientException

This commit is contained in:
Nicolas Grekas 2020-10-12 18:02:59 +02:00
parent 6000e51629
commit 0159a39709
6 changed files with 87 additions and 50 deletions

View 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();
}
}

View File

@ -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();
} 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); $this->doDestruct();
curl_setopt_array($this->handle, [
\CURLOPT_NOPROGRESS => true,
\CURLOPT_PROGRESSFUNCTION => null,
\CURLOPT_HEADERFUNCTION => null,
\CURLOPT_WRITEFUNCTION => null,
\CURLOPT_READFUNCTION => null,
\CURLOPT_INFILE => null,
]);
} }
/** /**

View File

@ -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;
} }
/** /**

View File

@ -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.

View File

@ -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);

View File

@ -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;