[HttpClient] fixes
This commit is contained in:
parent
790854989e
commit
3eca2b448d
@ -256,7 +256,7 @@ final class CurlHttpClient implements HttpClientInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new CurlResponse($this->multi, $ch, $options, self::createRedirectResolver($options, $host));
|
return new CurlResponse($this->multi, $ch, $options, $method, self::createRedirectResolver($options, $host));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -361,7 +361,7 @@ final class CurlHttpClient implements HttpClientInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
return static function ($ch, string $location) use ($redirectHeaders) {
|
return static function ($ch, string $location) use ($redirectHeaders) {
|
||||||
if ($host = parse_url($location, PHP_URL_HOST)) {
|
if ($redirectHeaders && $host = parse_url($location, PHP_URL_HOST)) {
|
||||||
$rawHeaders = $redirectHeaders['host'] === $host ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
|
$rawHeaders = $redirectHeaders['host'] === $host ? $redirectHeaders['with_auth'] : $redirectHeaders['no_auth'];
|
||||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $rawHeaders);
|
curl_setopt($ch, CURLOPT_HTTPHEADER, $rawHeaders);
|
||||||
}
|
}
|
||||||
|
@ -201,10 +201,16 @@ trait HttpClientTrait
|
|||||||
if ($r->isGenerator()) {
|
if ($r->isGenerator()) {
|
||||||
$body = $body(self::$CHUNK_SIZE);
|
$body = $body(self::$CHUNK_SIZE);
|
||||||
$body = function () use ($body) {
|
$body = function () use ($body) {
|
||||||
$chunk = $body->valid() ? $body->current() : '';
|
while ($body->valid()) {
|
||||||
$body->next();
|
$chunk = $body->current();
|
||||||
|
$body->next();
|
||||||
|
|
||||||
return $chunk;
|
if ('' !== $chunk) {
|
||||||
|
return $chunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,6 +97,7 @@ final class NativeHttpClient implements HttpClientInterface
|
|||||||
'raw_headers' => [],
|
'raw_headers' => [],
|
||||||
'url' => $url,
|
'url' => $url,
|
||||||
'error' => null,
|
'error' => null,
|
||||||
|
'http_method' => $method,
|
||||||
'http_code' => 0,
|
'http_code' => 0,
|
||||||
'redirect_count' => 0,
|
'redirect_count' => 0,
|
||||||
'start_time' => 0.0,
|
'start_time' => 0.0,
|
||||||
@ -336,8 +337,8 @@ final class NativeHttpClient implements HttpClientInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return static function (\stdClass $multi, int $statusCode, ?string $location, $context) use ($redirectHeaders, $proxy, $noProxy, &$info, $maxRedirects, $onProgress): ?string {
|
return static function (\stdClass $multi, ?string $location, $context) use ($redirectHeaders, $proxy, $noProxy, &$info, $maxRedirects, $onProgress): ?string {
|
||||||
if (null === $location || $statusCode < 300 || 400 <= $statusCode) {
|
if (null === $location || $info['http_code'] < 300 || 400 <= $info['http_code']) {
|
||||||
$info['redirect_url'] = null;
|
$info['redirect_url'] = null;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@ -356,11 +357,11 @@ final class NativeHttpClient implements HttpClientInterface
|
|||||||
$info['redirect_time'] = $now - $info['start_time'];
|
$info['redirect_time'] = $now - $info['start_time'];
|
||||||
|
|
||||||
// Do like curl and browsers: turn POST to GET on 301, 302 and 303
|
// Do like curl and browsers: turn POST to GET on 301, 302 and 303
|
||||||
if (\in_array($statusCode, [301, 302, 303], true)) {
|
if (\in_array($info['http_code'], [301, 302, 303], true)) {
|
||||||
$options = stream_context_get_options($context)['http'];
|
$options = stream_context_get_options($context)['http'];
|
||||||
|
|
||||||
if ('POST' === $options['method'] || 303 === $statusCode) {
|
if ('POST' === $options['method'] || 303 === $info['http_code']) {
|
||||||
$options['method'] = 'HEAD' === $options['method'] ? 'HEAD' : 'GET';
|
$info['http_method'] = $options['method'] = 'HEAD' === $options['method'] ? 'HEAD' : 'GET';
|
||||||
$options['content'] = '';
|
$options['content'] = '';
|
||||||
$options['header'] = array_filter($options['header'], static function ($h) {
|
$options['header'] = array_filter($options['header'], static function ($h) {
|
||||||
return 0 !== stripos($h, 'Content-Length:') && 0 !== stripos($h, 'Content-Type:');
|
return 0 !== stripos($h, 'Content-Length:') && 0 !== stripos($h, 'Content-Type:');
|
||||||
|
@ -29,7 +29,7 @@ final class CurlResponse implements ResponseInterface
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
public function __construct(\stdClass $multi, $ch, array $options = null, callable $resolveRedirect = null)
|
public function __construct(\stdClass $multi, $ch, array $options = null, string $method = 'GET', callable $resolveRedirect = null)
|
||||||
{
|
{
|
||||||
$this->multi = $multi;
|
$this->multi = $multi;
|
||||||
|
|
||||||
@ -42,9 +42,11 @@ final class CurlResponse implements ResponseInterface
|
|||||||
|
|
||||||
$this->id = $id = (int) $ch;
|
$this->id = $id = (int) $ch;
|
||||||
$this->timeout = $options['timeout'] ?? null;
|
$this->timeout = $options['timeout'] ?? null;
|
||||||
|
$this->info['http_method'] = $method;
|
||||||
$this->info['user_data'] = $options['user_data'] ?? null;
|
$this->info['user_data'] = $options['user_data'] ?? null;
|
||||||
$this->info['start_time'] = $this->info['start_time'] ?? microtime(true);
|
$this->info['start_time'] = $this->info['start_time'] ?? microtime(true);
|
||||||
$info = &$this->info;
|
$info = &$this->info;
|
||||||
|
$headers = &$this->headers;
|
||||||
|
|
||||||
if (!$info['raw_headers']) {
|
if (!$info['raw_headers']) {
|
||||||
// Used to keep track of what we're waiting for
|
// Used to keep track of what we're waiting for
|
||||||
@ -62,8 +64,8 @@ final class CurlResponse implements ResponseInterface
|
|||||||
$content = ($options['buffer'] ?? true) ? $content : null;
|
$content = ($options['buffer'] ?? true) ? $content : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
curl_setopt($ch, CURLOPT_HEADERFUNCTION, static function ($ch, string $data) use (&$info, $options, $multi, $id, &$location, $resolveRedirect): int {
|
curl_setopt($ch, CURLOPT_HEADERFUNCTION, static function ($ch, string $data) use (&$info, &$headers, $options, $multi, $id, &$location, $resolveRedirect): int {
|
||||||
return self::parseHeaderLine($ch, $data, $info, $options, $multi, $id, $location, $resolveRedirect);
|
return self::parseHeaderLine($ch, $data, $info, $headers, $options, $multi, $id, $location, $resolveRedirect);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (null === $options) {
|
if (null === $options) {
|
||||||
@ -116,8 +118,6 @@ final class CurlResponse implements ResponseInterface
|
|||||||
curl_setopt($ch, CURLOPT_HEADERFUNCTION, null);
|
curl_setopt($ch, CURLOPT_HEADERFUNCTION, null);
|
||||||
curl_setopt($ch, CURLOPT_READFUNCTION, null);
|
curl_setopt($ch, CURLOPT_READFUNCTION, null);
|
||||||
curl_setopt($ch, CURLOPT_INFILE, null);
|
curl_setopt($ch, CURLOPT_INFILE, null);
|
||||||
|
|
||||||
$response->addRawHeaders($response->info['raw_headers']);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Schedule the request in a non-blocking way
|
// Schedule the request in a non-blocking way
|
||||||
@ -243,7 +243,7 @@ final class CurlResponse implements ResponseInterface
|
|||||||
/**
|
/**
|
||||||
* Parses header lines as curl yields them to us.
|
* Parses header lines as curl yields them to us.
|
||||||
*/
|
*/
|
||||||
private static function parseHeaderLine($ch, string $data, array &$info, ?array $options, \stdClass $multi, int $id, ?string &$location, ?callable $resolveRedirect): int
|
private static function parseHeaderLine($ch, string $data, array &$info, array &$headers, ?array $options, \stdClass $multi, int $id, ?string &$location, ?callable $resolveRedirect): int
|
||||||
{
|
{
|
||||||
if (!\in_array($waitFor = @curl_getinfo($ch, CURLINFO_PRIVATE), ['headers', 'destruct'], true)) {
|
if (!\in_array($waitFor = @curl_getinfo($ch, CURLINFO_PRIVATE), ['headers', 'destruct'], true)) {
|
||||||
return \strlen($data); // Ignore HTTP trailers
|
return \strlen($data); // Ignore HTTP trailers
|
||||||
@ -251,7 +251,16 @@ final class CurlResponse implements ResponseInterface
|
|||||||
|
|
||||||
if ("\r\n" !== $data) {
|
if ("\r\n" !== $data) {
|
||||||
// Regular header line: add it to the list
|
// Regular header line: add it to the list
|
||||||
$info['raw_headers'][] = substr($data, 0, -2);
|
self::addRawHeaders([substr($data, 0, -2)], $info, $headers);
|
||||||
|
|
||||||
|
if (0 === strpos($data, 'HTTP') && 300 <= $info['http_code'] && $info['http_code'] < 400) {
|
||||||
|
if (curl_getinfo($ch, CURLINFO_REDIRECT_COUNT) === $options['max_redirects']) {
|
||||||
|
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
|
||||||
|
} elseif (303 === $info['http_code'] || ('POST' === $info['http_method'] && \in_array($info['http_code'], [301, 302], true))) {
|
||||||
|
$info['http_method'] = 'HEAD' === $info['http_method'] ? 'HEAD' : 'GET';
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (0 === stripos($data, 'Location:')) {
|
if (0 === stripos($data, 'Location:')) {
|
||||||
$location = trim(substr($data, 9, -2));
|
$location = trim(substr($data, 9, -2));
|
||||||
|
@ -111,11 +111,10 @@ final class NativeResponse implements ResponseInterface
|
|||||||
// Send request and follow redirects when needed
|
// Send request and follow redirects when needed
|
||||||
$this->info['fopen_time'] = microtime(true);
|
$this->info['fopen_time'] = microtime(true);
|
||||||
$this->handle = $h = fopen($url, 'r', false, $this->context);
|
$this->handle = $h = fopen($url, 'r', false, $this->context);
|
||||||
$this->addRawHeaders($http_response_header);
|
self::addRawHeaders($http_response_header, $this->info, $this->headers);
|
||||||
$url = ($this->resolveRedirect)($this->multi, $this->statusCode, $this->headers['location'][0] ?? null, $this->context);
|
$url = ($this->resolveRedirect)($this->multi, $this->headers['location'][0] ?? null, $this->context);
|
||||||
} while (null !== $url);
|
} while (null !== $url);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
$this->statusCode = 0;
|
|
||||||
$this->close();
|
$this->close();
|
||||||
$this->multi->handlesActivity[$this->id][] = null;
|
$this->multi->handlesActivity[$this->id][] = null;
|
||||||
$this->multi->handlesActivity[$this->id][] = $e;
|
$this->multi->handlesActivity[$this->id][] = $e;
|
||||||
|
@ -28,7 +28,6 @@ use Symfony\Component\HttpClient\Exception\TransportException;
|
|||||||
*/
|
*/
|
||||||
trait ResponseTrait
|
trait ResponseTrait
|
||||||
{
|
{
|
||||||
private $statusCode = 0;
|
|
||||||
private $headers = [];
|
private $headers = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -43,6 +42,7 @@ trait ResponseTrait
|
|||||||
|
|
||||||
private $info = [
|
private $info = [
|
||||||
'raw_headers' => [],
|
'raw_headers' => [],
|
||||||
|
'http_code' => 0,
|
||||||
'error' => null,
|
'error' => null,
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -63,7 +63,7 @@ trait ResponseTrait
|
|||||||
$this->initializer = null;
|
$this->initializer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->statusCode;
|
return $this->info['http_code'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -141,35 +141,35 @@ trait ResponseTrait
|
|||||||
*/
|
*/
|
||||||
abstract protected static function select(\stdClass $multi, float $timeout): int;
|
abstract protected static function select(\stdClass $multi, float $timeout): int;
|
||||||
|
|
||||||
private function addRawHeaders(array $rawHeaders): void
|
private static function addRawHeaders(array $rawHeaders, array &$info, array &$headers): void
|
||||||
{
|
{
|
||||||
foreach ($rawHeaders as $h) {
|
foreach ($rawHeaders as $h) {
|
||||||
if (11 <= \strlen($h) && '/' === $h[4] && preg_match('#^HTTP/\d+(?:\.\d+)? ([12345]\d\d) .*#', $h, $m)) {
|
if (11 <= \strlen($h) && '/' === $h[4] && preg_match('#^HTTP/\d+(?:\.\d+)? ([12345]\d\d) .*#', $h, $m)) {
|
||||||
$this->headers = [];
|
$headers = [];
|
||||||
$this->info['http_code'] = $this->statusCode = (int) $m[1];
|
$info['http_code'] = (int) $m[1];
|
||||||
} elseif (2 === \count($m = explode(':', $h, 2))) {
|
} elseif (2 === \count($m = explode(':', $h, 2))) {
|
||||||
$this->headers[strtolower($m[0])][] = ltrim($m[1]);
|
$headers[strtolower($m[0])][] = ltrim($m[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->info['raw_headers'][] = $h;
|
$info['raw_headers'][] = $h;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$this->statusCode) {
|
if (!$info['http_code']) {
|
||||||
throw new TransportException('Invalid or missing HTTP status line.');
|
throw new TransportException('Invalid or missing HTTP status line.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function checkStatusCode()
|
private function checkStatusCode()
|
||||||
{
|
{
|
||||||
if (500 <= $this->statusCode) {
|
if (500 <= $this->info['http_code']) {
|
||||||
throw new ServerException($this);
|
throw new ServerException($this);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (400 <= $this->statusCode) {
|
if (400 <= $this->info['http_code']) {
|
||||||
throw new ClientException($this);
|
throw new ClientException($this);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (300 <= $this->statusCode) {
|
if (300 <= $this->info['http_code']) {
|
||||||
throw new RedirectionException($this);
|
throw new RedirectionException($this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,6 +71,7 @@ interface ResponseInterface
|
|||||||
* - redirect_count - the number of redirects followed while executing the request
|
* - redirect_count - the number of redirects followed while executing the request
|
||||||
* - redirect_url - the resolved location of redirect responses, null otherwise
|
* - redirect_url - the resolved location of redirect responses, null otherwise
|
||||||
* - start_time - the time when the request was sent or 0.0 when it's pending
|
* - start_time - the time when the request was sent or 0.0 when it's pending
|
||||||
|
* - http_method - the HTTP verb of the last request
|
||||||
* - http_code - the last response code or 0 when it is not known yet
|
* - http_code - the last response code or 0 when it is not known yet
|
||||||
* - error - the error message when the transfer was aborted, null otherwise
|
* - error - the error message when the transfer was aborted, null otherwise
|
||||||
* - data - the value of the "data" request option, null if not set
|
* - data - the value of the "data" request option, null if not set
|
||||||
|
@ -214,7 +214,9 @@ abstract class HttpClientTestCase extends TestCase
|
|||||||
$client = $this->getHttpClient();
|
$client = $this->getHttpClient();
|
||||||
$response = $client->request('POST', 'http://localhost:8057/301', [
|
$response = $client->request('POST', 'http://localhost:8057/301', [
|
||||||
'auth' => 'foo:bar',
|
'auth' => 'foo:bar',
|
||||||
'body' => 'foo=bar',
|
'body' => function () {
|
||||||
|
yield 'foo=bar';
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$body = json_decode($response->getContent(), true);
|
$body = json_decode($response->getContent(), true);
|
||||||
@ -236,7 +238,9 @@ abstract class HttpClientTestCase extends TestCase
|
|||||||
'Content-Type: application/json',
|
'Content-Type: application/json',
|
||||||
];
|
];
|
||||||
|
|
||||||
$filteredHeaders = array_intersect($expected, $response->getInfo('raw_headers'));
|
$filteredHeaders = array_values(array_filter($response->getInfo('raw_headers'), function ($h) {
|
||||||
|
return \in_array(substr($h, 0, 4), ['HTTP', 'Loca', 'Cont'], true) && 'Content-Encoding: gzip' !== $h;
|
||||||
|
}));
|
||||||
|
|
||||||
$this->assertSame($expected, $filteredHeaders);
|
$this->assertSame($expected, $filteredHeaders);
|
||||||
}
|
}
|
||||||
@ -261,6 +265,16 @@ abstract class HttpClientTestCase extends TestCase
|
|||||||
public function testRedirect307()
|
public function testRedirect307()
|
||||||
{
|
{
|
||||||
$client = $this->getHttpClient();
|
$client = $this->getHttpClient();
|
||||||
|
|
||||||
|
$response = $client->request('POST', 'http://localhost:8057/307', [
|
||||||
|
'body' => function () {
|
||||||
|
yield 'foo=bar';
|
||||||
|
},
|
||||||
|
'max_redirects' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame(307, $response->getStatusCode());
|
||||||
|
|
||||||
$response = $client->request('POST', 'http://localhost:8057/307', [
|
$response = $client->request('POST', 'http://localhost:8057/307', [
|
||||||
'body' => 'foo=bar',
|
'body' => 'foo=bar',
|
||||||
]);
|
]);
|
||||||
@ -297,7 +311,9 @@ abstract class HttpClientTestCase extends TestCase
|
|||||||
'Content-Type: application/json',
|
'Content-Type: application/json',
|
||||||
];
|
];
|
||||||
|
|
||||||
$filteredHeaders = array_intersect($expected, $response->getInfo('raw_headers'));
|
$filteredHeaders = array_values(array_filter($response->getInfo('raw_headers'), function ($h) {
|
||||||
|
return \in_array(substr($h, 0, 4), ['HTTP', 'Loca', 'Cont'], true);
|
||||||
|
}));
|
||||||
|
|
||||||
$this->assertSame($expected, $filteredHeaders);
|
$this->assertSame($expected, $filteredHeaders);
|
||||||
}
|
}
|
||||||
@ -416,6 +432,7 @@ abstract class HttpClientTestCase extends TestCase
|
|||||||
$response = $client->request('POST', 'http://localhost:8057/post', [
|
$response = $client->request('POST', 'http://localhost:8057/post', [
|
||||||
'body' => function () {
|
'body' => function () {
|
||||||
yield 'foo';
|
yield 'foo';
|
||||||
|
yield '';
|
||||||
yield '=';
|
yield '=';
|
||||||
yield '0123456789';
|
yield '0123456789';
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user