v2.1.3
This commit is contained in:
parent
3daee2df53
commit
19ef0566df
610 changed files with 21958 additions and 3112 deletions
|
|
@ -9,6 +9,7 @@ use GuzzleHttp\Promise\FulfilledPromise;
|
|||
use GuzzleHttp\Promise\PromiseInterface;
|
||||
use GuzzleHttp\Psr7;
|
||||
use GuzzleHttp\TransferStats;
|
||||
use GuzzleHttp\TransportSharing;
|
||||
use GuzzleHttp\Utils;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
|
@ -22,11 +23,49 @@ use Psr\Http\Message\UriInterface;
|
|||
*/
|
||||
class StreamHandler
|
||||
{
|
||||
private const CONNECTION_ERRORS = [
|
||||
'php_network_getaddresses:',
|
||||
'getaddrinfo',
|
||||
'gethostbyname failed',
|
||||
'Connection refused',
|
||||
'No connection could be made because the target machine actively refused it',
|
||||
"couldn't connect to host", // error on HHVM
|
||||
'connection attempt failed',
|
||||
'connect() failed',
|
||||
'Connection timed out',
|
||||
'Operation timed out',
|
||||
'Network is unreachable',
|
||||
'No route to host',
|
||||
'Host is unreachable',
|
||||
'Host is down',
|
||||
'Cannot connect to HTTPS server through proxy',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $lastHeaders = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $transportSharingMode;
|
||||
|
||||
/**
|
||||
* Accepts an associative array of options:
|
||||
*
|
||||
* - transport_sharing: Optional transport sharing mode.
|
||||
*
|
||||
* @param array{transport_sharing?: mixed} $options Array of options to use with the handler
|
||||
*/
|
||||
public function __construct(array $options = [])
|
||||
{
|
||||
$this->transportSharingMode = CurlShareHandleState::normalizeMode(
|
||||
$options['transport_sharing'] ?? null,
|
||||
'transport_sharing'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an HTTP request.
|
||||
*
|
||||
|
|
@ -42,12 +81,22 @@ class StreamHandler
|
|||
|
||||
$protocolVersion = $request->getProtocolVersion();
|
||||
|
||||
if ('' === $protocolVersion) {
|
||||
\trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Sending a request with an empty protocol version is deprecated; guzzlehttp/guzzle 8.0 will reject empty protocol versions.');
|
||||
|
||||
$protocolVersion = '1.1';
|
||||
$request = Psr7\Utils::modifyRequest($request, ['version' => $protocolVersion]);
|
||||
}
|
||||
|
||||
if ('1.0' !== $protocolVersion && '1.1' !== $protocolVersion) {
|
||||
throw new ConnectException(sprintf('HTTP/%s is not supported by the stream handler.', $protocolVersion), $request);
|
||||
}
|
||||
|
||||
$startTime = isset($options['on_stats']) ? Utils::currentTime() : null;
|
||||
|
||||
self::triggerUnsupportedRequestOptionDeprecations($request, $options);
|
||||
$this->assertTransportSharingSupported();
|
||||
|
||||
try {
|
||||
// Does not support the expect header.
|
||||
$request = $request->withoutHeader('Expect');
|
||||
|
|
@ -74,16 +123,10 @@ class StreamHandler
|
|||
throw $e;
|
||||
} catch (\Exception $e) {
|
||||
// Determine if the error was a networking error.
|
||||
$message = $e->getMessage();
|
||||
// This list can probably get more comprehensive.
|
||||
if (false !== \strpos($message, 'getaddrinfo') // DNS lookup failed
|
||||
|| false !== \strpos($message, 'Connection refused')
|
||||
|| false !== \strpos($message, "couldn't connect to host") // error on HHVM
|
||||
|| false !== \strpos($message, 'connection attempt failed')
|
||||
) {
|
||||
if (self::isConnectionError($e->getMessage())) {
|
||||
$e = new ConnectException($e->getMessage(), $request, $e);
|
||||
} else {
|
||||
$e = RequestException::wrapException($request, $e);
|
||||
$e = $e instanceof RequestException ? $e : new RequestException($e->getMessage(), $request, null, $e);
|
||||
}
|
||||
$this->invokeStats($options, $request, $startTime, null, $e);
|
||||
|
||||
|
|
@ -91,6 +134,17 @@ class StreamHandler
|
|||
}
|
||||
}
|
||||
|
||||
private static function isConnectionError(string $message): bool
|
||||
{
|
||||
foreach (self::CONNECTION_ERRORS as $connectionError) {
|
||||
if (false !== \strpos($message, $connectionError)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function invokeStats(
|
||||
array $options,
|
||||
RequestInterface $request,
|
||||
|
|
@ -114,10 +168,8 @@ class StreamHandler
|
|||
|
||||
try {
|
||||
[$ver, $status, $reason, $headers] = HeaderProcessor::parseHeaders($hdrs);
|
||||
} catch (\Exception $e) {
|
||||
return P\Create::rejectionFor(
|
||||
new RequestException('An error was encountered while creating the response', $request, null, $e)
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->rejectResponseCreation($options, $request, $startTime, $e);
|
||||
}
|
||||
|
||||
[$stream, $headers] = $this->checkDecode($options, $headers, $stream);
|
||||
|
|
@ -130,16 +182,14 @@ class StreamHandler
|
|||
|
||||
try {
|
||||
$response = new Psr7\Response($status, $headers, $sink, $ver, $reason);
|
||||
} catch (\Exception $e) {
|
||||
return P\Create::rejectionFor(
|
||||
new RequestException('An error was encountered while creating the response', $request, null, $e)
|
||||
);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->rejectResponseCreation($options, $request, $startTime, $e);
|
||||
}
|
||||
|
||||
if (isset($options['on_headers'])) {
|
||||
try {
|
||||
$options['on_headers']($response);
|
||||
} catch (\Exception $e) {
|
||||
} catch (\Throwable $e) {
|
||||
return P\Create::rejectionFor(
|
||||
new RequestException('An error was encountered during the on_headers event', $request, $response, $e)
|
||||
);
|
||||
|
|
@ -157,6 +207,24 @@ class StreamHandler
|
|||
return new FulfilledPromise($response);
|
||||
}
|
||||
|
||||
private function rejectResponseCreation(
|
||||
array $options,
|
||||
RequestInterface $request,
|
||||
?float $startTime,
|
||||
\Throwable $previous
|
||||
): PromiseInterface {
|
||||
$reason = new RequestException(
|
||||
'An error was encountered while creating the response',
|
||||
$request,
|
||||
null,
|
||||
$previous
|
||||
);
|
||||
|
||||
$this->invokeStats($options, $request, $startTime, null, $reason);
|
||||
|
||||
return P\Create::rejectionFor($reason);
|
||||
}
|
||||
|
||||
private function createSink(StreamInterface $stream, array $options): StreamInterface
|
||||
{
|
||||
if (!empty($options['stream'])) {
|
||||
|
|
@ -185,15 +253,12 @@ class StreamHandler
|
|||
// Remove content-encoding header
|
||||
unset($headers[$normalizedKeys['content-encoding']]);
|
||||
|
||||
// Fix content-length header
|
||||
// The decoded length cannot be known without inflating the
|
||||
// stream, so keep the original length for inspection and
|
||||
// drop the now-unknown Content-Length header.
|
||||
if (isset($normalizedKeys['content-length'])) {
|
||||
$headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']];
|
||||
$length = (int) $stream->getSize();
|
||||
if ($length === 0) {
|
||||
unset($headers[$normalizedKeys['content-length']]);
|
||||
} else {
|
||||
$headers[$normalizedKeys['content-length']] = [$length];
|
||||
}
|
||||
unset($headers[$normalizedKeys['content-length']]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -279,8 +344,14 @@ class StreamHandler
|
|||
$methods = \array_flip(\get_class_methods(__CLASS__));
|
||||
}
|
||||
|
||||
if (!\in_array($request->getUri()->getScheme(), ['http', 'https'])) {
|
||||
throw new RequestException(\sprintf("The scheme '%s' is not supported.", $request->getUri()->getScheme()), $request);
|
||||
$scheme = $request->getUri()->getScheme();
|
||||
if (!\in_array($scheme, ['http', 'https'], true)) {
|
||||
throw new RequestException(\sprintf("The scheme '%s' is not supported.", $scheme), $request);
|
||||
}
|
||||
|
||||
$protocols = Utils::normalizeProtocols($options['protocols'] ?? ['http', 'https']);
|
||||
if (!\in_array($scheme, $protocols, true)) {
|
||||
throw new RequestException(\sprintf('The scheme "%s" is not allowed by the protocols request option.', $scheme), $request);
|
||||
}
|
||||
|
||||
// HTTP/1.1 streams using the PHP stream wrapper require a
|
||||
|
|
@ -338,7 +409,6 @@ class StreamHandler
|
|||
|
||||
// See https://wiki.php.net/rfc/deprecations_php_8_5#deprecate_the_http_response_header_predefined_variable
|
||||
if (function_exists('http_get_last_response_headers')) {
|
||||
/** @var array|null */
|
||||
$http_response_header = \http_get_last_response_headers();
|
||||
}
|
||||
|
||||
|
|
@ -423,6 +493,121 @@ class StreamHandler
|
|||
return $context;
|
||||
}
|
||||
|
||||
private static function triggerUnsupportedRequestOptionDeprecations(RequestInterface $request, array $options): void
|
||||
{
|
||||
if (
|
||||
\array_key_exists('curl', $options)
|
||||
&& $options['curl'] !== null
|
||||
&& $options['curl'] !== []
|
||||
&& !self::isCurlOptionGeneratedByAuth($options)
|
||||
) {
|
||||
\trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Passing the "curl" request option to the stream handler is deprecated; guzzlehttp/guzzle 8.0 will reject this option because the stream handler ignores cURL options.');
|
||||
}
|
||||
|
||||
if (self::usesDigestAuth($options)) {
|
||||
\trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Passing digest authentication to the stream handler is deprecated; guzzlehttp/guzzle 8.0 will reject digest authentication with the stream handler because it is only supported by cURL handlers.');
|
||||
}
|
||||
|
||||
if (\array_key_exists('expect', $options) && $options['expect'] !== false && $request->hasHeader('Expect')) {
|
||||
\trigger_deprecation('guzzlehttp/guzzle', '7.11', 'Passing the "expect" request option to the stream handler is deprecated when it adds an Expect header; guzzlehttp/guzzle 8.0 will reject this option because the stream handler does not support Expect: 100-Continue.');
|
||||
}
|
||||
}
|
||||
|
||||
private function assertTransportSharingSupported(): void
|
||||
{
|
||||
if ($this->transportSharingMode === TransportSharing::HANDLER_REQUIRE) {
|
||||
throw new \InvalidArgumentException('The "transport_sharing" option requires transport sharing, but the stream handler does not support it.');
|
||||
}
|
||||
}
|
||||
|
||||
private static function isCurlOptionGeneratedByAuth(array $options): bool
|
||||
{
|
||||
if (!isset($options['curl']) || !\is_array($options['curl']) || !isset($options['auth'][2]) || !\is_string($options['auth'][2])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!\defined('CURLOPT_HTTPAUTH') || !\defined('CURLOPT_USERPWD')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$type = \strtolower($options['auth'][2]);
|
||||
if ($type === 'digest') {
|
||||
$httpAuth = \defined('CURLAUTH_DIGEST') ? \constant('CURLAUTH_DIGEST') : null;
|
||||
} elseif ($type === 'ntlm') {
|
||||
$httpAuth = \defined('CURLAUTH_NTLM') ? \constant('CURLAUTH_NTLM') : null;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $httpAuth !== null
|
||||
&& \count($options['curl']) === 2
|
||||
&& isset($options['curl'][\CURLOPT_HTTPAUTH], $options['curl'][\CURLOPT_USERPWD])
|
||||
&& $options['curl'][\CURLOPT_HTTPAUTH] === $httpAuth;
|
||||
}
|
||||
|
||||
private static function usesDigestAuth(array $options): bool
|
||||
{
|
||||
return isset($options['auth'][2])
|
||||
&& \is_string($options['auth'][2])
|
||||
&& \strtolower($options['auth'][2]) === 'digest';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value as passed via Request transfer options.
|
||||
*
|
||||
* @return array{0: string, 1: string|null}
|
||||
*/
|
||||
private static function normalizeTlsFileOption(string $option, $value): array
|
||||
{
|
||||
$passphrase = null;
|
||||
|
||||
if (\is_array($value)) {
|
||||
if (!isset($value[0]) || !\is_string($value[0])) {
|
||||
throw new \InvalidArgumentException(\sprintf('Invalid %s request option', $option));
|
||||
}
|
||||
if (isset($value[1])) {
|
||||
if (!\is_string($value[1])) {
|
||||
throw new \InvalidArgumentException(\sprintf('Invalid %s request option', $option));
|
||||
}
|
||||
$passphrase = $value[1];
|
||||
}
|
||||
$value = $value[0];
|
||||
}
|
||||
|
||||
if (!\is_string($value)) {
|
||||
throw new \InvalidArgumentException(\sprintf('Invalid %s request option', $option));
|
||||
}
|
||||
|
||||
return [$value, $passphrase];
|
||||
}
|
||||
|
||||
private static function setTlsPassphrase(array &$options, ?string $passphrase, string $option): void
|
||||
{
|
||||
if ($passphrase === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($options['ssl']['passphrase']) && $options['ssl']['passphrase'] !== $passphrase) {
|
||||
throw new \InvalidArgumentException(\sprintf('Cannot use different passphrases for cert and ssl_key with the stream handler; %s conflicts with an existing TLS passphrase.', $option));
|
||||
}
|
||||
|
||||
$options['ssl']['passphrase'] = $passphrase;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value as passed via Request transfer options.
|
||||
*/
|
||||
private static function assertStreamTlsType(string $option, $value): void
|
||||
{
|
||||
if (!\is_string($value) || $value === '') {
|
||||
throw new \InvalidArgumentException(\sprintf('%s must be a non-empty string', $option));
|
||||
}
|
||||
|
||||
if (\strtoupper($value) !== 'PEM') {
|
||||
throw new \InvalidArgumentException(\sprintf('The stream handler only supports "PEM" for the %s request option.', $option));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value as passed via Request transfer options.
|
||||
*/
|
||||
|
|
@ -435,7 +620,10 @@ class StreamHandler
|
|||
} else {
|
||||
$scheme = $request->getUri()->getScheme();
|
||||
if (isset($value[$scheme])) {
|
||||
if (!isset($value['no']) || !Utils::isHostInNoProxy($request->getUri()->getHost(), $value['no'])) {
|
||||
if (
|
||||
!isset($value['no'])
|
||||
|| !Utils::isUriInNoProxy($request->getUri(), $value['no'])
|
||||
) {
|
||||
$uri = $value[$scheme];
|
||||
}
|
||||
}
|
||||
|
|
@ -544,23 +732,56 @@ class StreamHandler
|
|||
*/
|
||||
private function add_cert(RequestInterface $request, array &$options, $value, array &$params): void
|
||||
{
|
||||
if (\is_array($value)) {
|
||||
$options['ssl']['passphrase'] = $value[1];
|
||||
$value = $value[0];
|
||||
}
|
||||
[$value, $passphrase] = self::normalizeTlsFileOption('cert', $value);
|
||||
|
||||
if (!\file_exists($value)) {
|
||||
throw new \RuntimeException("SSL certificate not found: {$value}");
|
||||
}
|
||||
|
||||
self::setTlsPassphrase($options, $passphrase, 'cert');
|
||||
$options['ssl']['local_cert'] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value as passed via Request transfer options.
|
||||
*/
|
||||
private function add_cert_type(RequestInterface $request, array &$options, $value, array &$params): void
|
||||
{
|
||||
self::assertStreamTlsType('cert_type', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value as passed via Request transfer options.
|
||||
*/
|
||||
private function add_ssl_key(RequestInterface $request, array &$options, $value, array &$params): void
|
||||
{
|
||||
[$value, $passphrase] = self::normalizeTlsFileOption('ssl_key', $value);
|
||||
|
||||
if (!\file_exists($value)) {
|
||||
throw new \RuntimeException("SSL private key not found: {$value}");
|
||||
}
|
||||
|
||||
self::setTlsPassphrase($options, $passphrase, 'ssl_key');
|
||||
$options['ssl']['local_pk'] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value as passed via Request transfer options.
|
||||
*/
|
||||
private function add_ssl_key_type(RequestInterface $request, array &$options, $value, array &$params): void
|
||||
{
|
||||
self::assertStreamTlsType('ssl_key_type', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value as passed via Request transfer options.
|
||||
*/
|
||||
private function add_progress(RequestInterface $request, array &$options, $value, array &$params): void
|
||||
{
|
||||
if (!\is_callable($value)) {
|
||||
throw new \InvalidArgumentException('progress client option must be callable');
|
||||
}
|
||||
|
||||
self::addNotification(
|
||||
$params,
|
||||
static function ($code, $a, $b, $c, $transferred, $total) use ($value) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue