[HttpClient] make DNS resolution lazy with NativeHttpClient

This commit is contained in:
Nicolas Grekas 2020-06-10 23:35:28 +02:00
parent 9b08626180
commit d3a450353d
5 changed files with 39 additions and 17 deletions

View File

@ -171,12 +171,6 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac
$this->logger && $this->logger->info(sprintf('Request: "%s %s"', $method, implode('', $url)));
[$host, $port, $url['authority']] = self::dnsResolve($url, $this->multi, $info, $onProgress);
if (!isset($options['normalized_headers']['host'])) {
$options['headers'][] = 'Host: '.$host.$port;
}
if (!isset($options['normalized_headers']['user-agent'])) {
$options['headers'][] = 'User-Agent: Symfony HttpClient/Native';
}
@ -198,7 +192,6 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac
'follow_location' => false, // We follow redirects ourselves - the native logic is too limited
],
'ssl' => array_filter([
'peer_name' => $host,
'verify_peer' => $options['verify_peer'],
'verify_peer_name' => $options['verify_host'],
'cafile' => $options['cafile'],
@ -219,12 +212,23 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac
],
];
$proxy = self::getProxy($options['proxy'], $url, $options['no_proxy']);
$resolveRedirect = self::createRedirectResolver($options, $host, $proxy, $info, $onProgress);
$context = stream_context_create($context, ['notification' => $notification]);
self::configureHeadersAndProxy($context, $host, $options['headers'], $proxy);
return new NativeResponse($this->multi, $context, implode('', $url), $options, $info, $resolveRedirect, $onProgress, $this->logger);
$resolver = static function($multi) use ($context, $options, $url, &$info, $onProgress) {
[$host, $port, $url['authority']] = self::dnsResolve($url, $multi, $info, $onProgress);
if (!isset($options['normalized_headers']['host'])) {
$options['headers'][] = 'Host: '.$host.$port;
}
stream_context_set_option($context, 'ssl', 'peer_name', $host);
$proxy = self::getProxy($options['proxy'], $url, $options['no_proxy']);
self::configureHeadersAndProxy($context, $host, $options['headers'], $proxy);
return [self::createRedirectResolver($options, $host, $proxy, $info, $onProgress), implode('', $url)];
};
return new NativeResponse($this->multi, $context, implode('', $url), $options, $info, $resolver, $onProgress, $this->logger);
}
/**

View File

@ -94,7 +94,7 @@ final class CurlResponse implements ResponseInterface
if (0 < $duration) {
if ($execCounter === $multi->execCounter) {
$multi->execCounter = !\is_float($execCounter) ? 1 + $execCounter : PHP_INT_MIN;
curl_multi_exec($multi->handle, $execCounter);
curl_multi_remove_handle($multi->handle, $ch);
}
$lastExpiry = end($multi->pauseExpiries);
@ -106,6 +106,7 @@ final class CurlResponse implements ResponseInterface
} else {
unset($multi->pauseExpiries[(int) $ch]);
curl_pause($ch, CURLPAUSE_CONT);
curl_multi_add_handle($multi->handle, $ch);
}
};
@ -335,6 +336,7 @@ final class CurlResponse implements ResponseInterface
unset($multi->pauseExpiries[$id]);
curl_pause($multi->openHandles[$id][0], CURLPAUSE_CONT);
curl_multi_add_handle($multi->handle, $multi->openHandles[$id][0]);
}
}

View File

@ -31,7 +31,7 @@ final class NativeResponse implements ResponseInterface
private $context;
private $url;
private $resolveRedirect;
private $resolver;
private $onProgress;
private $remaining;
private $buffer;
@ -43,7 +43,7 @@ final class NativeResponse implements ResponseInterface
/**
* @internal
*/
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 $resolver, ?callable $onProgress, ?LoggerInterface $logger)
{
$this->multi = $multi;
$this->id = (int) $context;
@ -52,7 +52,7 @@ final class NativeResponse implements ResponseInterface
$this->logger = $logger;
$this->timeout = $options['timeout'];
$this->info = &$info;
$this->resolveRedirect = $resolveRedirect;
$this->resolver = $resolver;
$this->onProgress = $onProgress;
$this->inflate = !isset($options['normalized_headers']['accept-encoding']);
$this->shouldBuffer = $options['buffer'] ?? true;
@ -128,6 +128,8 @@ final class NativeResponse implements ResponseInterface
try {
$this->info['start_time'] = microtime(true);
[$resolver, $url] = ($this->resolver)($this->multi);
while (true) {
$context = stream_context_get_options($this->context);
@ -145,7 +147,7 @@ final class NativeResponse implements ResponseInterface
// Send request and follow redirects when needed
$this->handle = $h = fopen($url, 'r', false, $this->context);
self::addResponseHeaders($http_response_header, $this->info, $this->headers, $this->info['debug']);
$url = ($this->resolveRedirect)($this->multi, $this->headers['location'][0] ?? null, $this->context);
$url = $resolver($this->multi, $this->headers['location'][0] ?? null, $this->context);
if (null === $url) {
break;
@ -169,7 +171,7 @@ final class NativeResponse implements ResponseInterface
}
stream_set_blocking($h, false);
$this->context = $this->resolveRedirect = null;
$this->context = $this->resolver = null;
// Create dechunk buffers
if (isset($this->headers['content-length'])) {

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\HttpClient\Tests;
use Symfony\Component\HttpClient\Exception\ClientException;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpClient\Response\StreamWrapper;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;
@ -238,6 +239,15 @@ abstract class HttpClientTestCase extends BaseHttpClientTestCase
$this->assertSame($expected, $logger->logs);
}
public function testDnsFailure()
{
$client = $this->getHttpClient(__FUNCTION__);
$response = $client->request('GET', 'http://bad.host.test/');
$this->expectException(TransportException::class);
$response->getStatusCode();
}
private static function startVulcain(HttpClientInterface $client)
{
if (self::$vulcainStarted) {

View File

@ -202,6 +202,10 @@ class MockHttpClientTest extends HttpClientTestCase
$this->markTestSkipped("MockHttpClient doesn't support pauses by default");
break;
case 'testDnsFailure':
$this->markTestSkipped("MockHttpClient doesn't use a DNS");
break;
case 'testGetRequest':
array_unshift($headers, 'HTTP/1.1 200 OK');
$responses[] = new MockResponse($body, ['response_headers' => $headers]);