*\r
* LICENSE:\r
*\r
- * Copyright (c) 2008, 2009, Alexey Borzov <avb@php.net>\r
+ * Copyright (c) 2008-2011, Alexey Borzov <avb@php.net>\r
* All rights reserved.\r
*\r
* Redistribution and use in source and binary forms, with or without\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
* @license http://opensource.org/licenses/bsd-license.php New BSD License\r
- * @version CVS: $Id: Socket.php 279760 2009-05-03 10:46:42Z avb $\r
+ * @version SVN: $Id: Socket.php 309921 2011-04-03 16:43:02Z avb $\r
* @link http://pear.php.net/package/HTTP_Request2\r
*/\r
\r
* @category HTTP\r
* @package HTTP_Request2\r
* @author Alexey Borzov <avb@php.net>\r
- * @version Release: 0.4.1\r
+ * @version Release: 2.0.0RC1\r
*/\r
class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter\r
{\r
/**\r
* Regular expression for 'token' rule from RFC 2616\r
- */ \r
+ */\r
const REGEXP_TOKEN = '[^\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]+';\r
\r
/**\r
/**\r
* Data for digest authentication scheme\r
*\r
- * The keys for the array are URL prefixes. \r
+ * The keys for the array are URL prefixes.\r
*\r
- * The values are associative arrays with data (realm, nonce, nonce-count, \r
- * opaque...) needed for digest authentication. Stored here to prevent making \r
- * duplicate requests to digest-protected resources after we have already \r
+ * The values are associative arrays with data (realm, nonce, nonce-count,\r
+ * opaque...) needed for digest authentication. Stored here to prevent making\r
+ * duplicate requests to digest-protected resources after we have already\r
* received the challenge.\r
*\r
* @var array\r
protected $proxyChallenge;\r
\r
/**\r
- * Global timeout, exception will be raised if request continues past this time\r
+ * Sum of start time and global timeout, exception will be thrown if request continues past this time\r
* @var integer\r
*/\r
- protected $timeout = null;\r
+ protected $deadline = null;\r
\r
/**\r
* Remaining length of the current chunk, when reading chunked response\r
* @var integer\r
* @see readChunked()\r
- */ \r
+ */\r
protected $chunkLength = 0;\r
\r
+ /**\r
+ * Remaining amount of redirections to follow\r
+ *\r
+ * Starts at 'max_redirects' configuration parameter and is reduced on each\r
+ * subsequent redirect. An Exception will be thrown once it reaches zero.\r
+ *\r
+ * @var integer\r
+ */\r
+ protected $redirectCountdown = null;\r
+\r
/**\r
* Sends request to the remote server and returns its response\r
*\r
public function sendRequest(HTTP_Request2 $request)\r
{\r
$this->request = $request;\r
- $keepAlive = $this->connect();\r
- $headers = $this->prepareHeaders();\r
\r
- // Use global request timeout if given, see feature requests #5735, #8964 \r
+ // Use global request timeout if given, see feature requests #5735, #8964\r
if ($timeout = $request->getConfig('timeout')) {\r
- $this->timeout = time() + $timeout;\r
+ $this->deadline = time() + $timeout;\r
} else {\r
- $this->timeout = null;\r
+ $this->deadline = null;\r
}\r
\r
try {\r
+ $keepAlive = $this->connect();\r
+ $headers = $this->prepareHeaders();\r
if (false === @fwrite($this->socket, $headers, strlen($headers))) {\r
- throw new HTTP_Request2_Exception('Error writing request');\r
+ throw new HTTP_Request2_MessageException('Error writing request');\r
}\r
// provide request headers to the observer, see request #7633\r
$this->request->setLastEvent('sentHeaders', $headers);\r
$this->writeBody();\r
\r
- if ($this->timeout && time() > $this->timeout) {\r
- throw new HTTP_Request2_Exception(\r
- 'Request timed out after ' . \r
- $request->getConfig('timeout') . ' second(s)'\r
+ if ($this->deadline && time() > $this->deadline) {\r
+ throw new HTTP_Request2_MessageException(\r
+ 'Request timed out after ' .\r
+ $request->getConfig('timeout') . ' second(s)',\r
+ HTTP_Request2_Exception::TIMEOUT\r
);\r
}\r
\r
$response = $this->readResponse();\r
\r
+ if ($jar = $request->getCookieJar()) {\r
+ $jar->addCookiesFromResponse($response, $request->getUrl());\r
+ }\r
+\r
if (!$this->canKeepAlive($keepAlive, $response)) {\r
$this->disconnect();\r
}\r
\r
} catch (Exception $e) {\r
$this->disconnect();\r
+ }\r
+\r
+ unset($this->request, $this->requestBody);\r
+\r
+ if (!empty($e)) {\r
+ $this->redirectCountdown = null;\r
throw $e;\r
}\r
\r
- return $response;\r
+ if (!$request->getConfig('follow_redirects') || !$response->isRedirect()) {\r
+ $this->redirectCountdown = null;\r
+ return $response;\r
+ } else {\r
+ return $this->handleRedirect($request, $response);\r
+ }\r
}\r
\r
/**\r
\r
if ($host = $this->request->getConfig('proxy_host')) {\r
if (!($port = $this->request->getConfig('proxy_port'))) {\r
- throw new HTTP_Request2_Exception('Proxy port not provided');\r
+ throw new HTTP_Request2_LogicException(\r
+ 'Proxy port not provided',\r
+ HTTP_Request2_Exception::MISSING_VALUE\r
+ );\r
}\r
$proxy = true;\r
} else {\r
}\r
\r
if ($tunnel && !$proxy) {\r
- throw new HTTP_Request2_Exception(\r
- "Trying to perform CONNECT request without proxy"\r
+ throw new HTTP_Request2_LogicException(\r
+ "Trying to perform CONNECT request without proxy",\r
+ HTTP_Request2_Exception::MISSING_VALUE\r
);\r
}\r
if ($secure && !in_array('ssl', stream_get_transports())) {\r
- throw new HTTP_Request2_Exception(\r
- 'Need OpenSSL support for https:// requests'\r
+ throw new HTTP_Request2_LogicException(\r
+ 'Need OpenSSL support for https:// requests',\r
+ HTTP_Request2_Exception::MISCONFIGURATION\r
);\r
}\r
\r
// RFC 2068, section 19.7.1: A client MUST NOT send the Keep-Alive\r
// connection token to a proxy server...\r
- if ($proxy && !$secure && \r
+ if ($proxy && !$secure &&\r
!empty($headers['connection']) && 'Keep-Alive' == $headers['connection']\r
) {\r
$this->request->setHeader('connection');\r
}\r
\r
- $keepAlive = ('1.1' == $this->request->getConfig('protocol_version') && \r
+ $keepAlive = ('1.1' == $this->request->getConfig('protocol_version') &&\r
empty($headers['connection'])) ||\r
(!empty($headers['connection']) &&\r
'Keep-Alive' == $headers['connection']);\r
$context = stream_context_create();\r
foreach ($options as $name => $value) {\r
if (!stream_context_set_option($context, 'ssl', $name, $value)) {\r
- throw new HTTP_Request2_Exception(\r
+ throw new HTTP_Request2_LogicException(\r
"Error setting SSL context option '{$name}'"\r
);\r
}\r
}\r
+ $track = @ini_set('track_errors', 1);\r
$this->socket = @stream_socket_client(\r
$remote, $errno, $errstr,\r
$this->request->getConfig('connect_timeout'),\r
STREAM_CLIENT_CONNECT, $context\r
);\r
if (!$this->socket) {\r
- throw new HTTP_Request2_Exception(\r
- "Unable to connect to {$remote}. Error #{$errno}: {$errstr}"\r
+ $e = new HTTP_Request2_ConnectionException(\r
+ "Unable to connect to {$remote}. Error: "\r
+ . (empty($errstr)? $php_errormsg: $errstr), 0, $errno\r
);\r
}\r
+ @ini_set('track_errors', $track);\r
+ if (isset($e)) {\r
+ throw $e;\r
+ }\r
$this->request->setLastEvent('connect', $remote);\r
self::$sockets[$socketKey] =& $this->socket;\r
}\r
$response = $connect->send();\r
// Need any successful (2XX) response\r
if (200 > $response->getStatus() || 300 <= $response->getStatus()) {\r
- throw new HTTP_Request2_Exception(\r
+ throw new HTTP_Request2_ConnectionException(\r
'Failed to connect via HTTPS proxy. Proxy response: ' .\r
$response->getStatus() . ' ' . $response->getReasonPhrase()\r
);\r
$this->socket = $donor->socket;\r
\r
$modes = array(\r
- STREAM_CRYPTO_METHOD_TLS_CLIENT, \r
+ STREAM_CRYPTO_METHOD_TLS_CLIENT,\r
STREAM_CRYPTO_METHOD_SSLv3_CLIENT,\r
STREAM_CRYPTO_METHOD_SSLv23_CLIENT,\r
- STREAM_CRYPTO_METHOD_SSLv2_CLIENT \r
+ STREAM_CRYPTO_METHOD_SSLv2_CLIENT\r
);\r
\r
foreach ($modes as $mode) {\r
return;\r
}\r
}\r
- throw new HTTP_Request2_Exception(\r
+ throw new HTTP_Request2_ConnectionException(\r
'Failed to enable secure connection when connecting through proxy'\r
);\r
}\r
/**\r
* Checks whether current connection may be reused or should be closed\r
*\r
- * @param boolean whether connection could be persistent \r
+ * @param boolean whether connection could be persistent\r
* in the first place\r
* @param HTTP_Request2_Response response object to check\r
* @return boolean\r
return true;\r
}\r
\r
- $lengthKnown = 'chunked' == strtolower($response->getHeader('transfer-encoding')) ||\r
- null !== $response->getHeader('content-length');\r
+ $lengthKnown = 'chunked' == strtolower($response->getHeader('transfer-encoding'))\r
+ || null !== $response->getHeader('content-length')\r
+ // no body possible for such responses, see also request #17031\r
+ || HTTP_Request2::METHOD_HEAD == $this->request->getMethod()\r
+ || in_array($response->getStatus(), array(204, 304));\r
$persistent = 'keep-alive' == strtolower($response->getHeader('connection')) ||\r
(null === $response->getHeader('connection') &&\r
'1.1' == $response->getVersion());\r
}\r
}\r
\r
+ /**\r
+ * Handles HTTP redirection\r
+ *\r
+ * This method will throw an Exception if redirect to a non-HTTP(S) location\r
+ * is attempted, also if number of redirects performed already is equal to\r
+ * 'max_redirects' configuration parameter.\r
+ *\r
+ * @param HTTP_Request2 Original request\r
+ * @param HTTP_Request2_Response Response containing redirect\r
+ * @return HTTP_Request2_Response Response from a new location\r
+ * @throws HTTP_Request2_Exception\r
+ */\r
+ protected function handleRedirect(HTTP_Request2 $request,\r
+ HTTP_Request2_Response $response)\r
+ {\r
+ if (is_null($this->redirectCountdown)) {\r
+ $this->redirectCountdown = $request->getConfig('max_redirects');\r
+ }\r
+ if (0 == $this->redirectCountdown) {\r
+ $this->redirectCountdown = null;\r
+ // Copying cURL behaviour\r
+ throw new HTTP_Request2_MessageException (\r
+ 'Maximum (' . $request->getConfig('max_redirects') . ') redirects followed',\r
+ HTTP_Request2_Exception::TOO_MANY_REDIRECTS\r
+ );\r
+ }\r
+ $redirectUrl = new Net_URL2(\r
+ $response->getHeader('location'),\r
+ array(Net_URL2::OPTION_USE_BRACKETS => $request->getConfig('use_brackets'))\r
+ );\r
+ // refuse non-HTTP redirect\r
+ if ($redirectUrl->isAbsolute()\r
+ && !in_array($redirectUrl->getScheme(), array('http', 'https'))\r
+ ) {\r
+ $this->redirectCountdown = null;\r
+ throw new HTTP_Request2_MessageException(\r
+ 'Refusing to redirect to a non-HTTP URL ' . $redirectUrl->__toString(),\r
+ HTTP_Request2_Exception::NON_HTTP_REDIRECT\r
+ );\r
+ }\r
+ // Theoretically URL should be absolute (see http://tools.ietf.org/html/rfc2616#section-14.30),\r
+ // but in practice it is often not\r
+ if (!$redirectUrl->isAbsolute()) {\r
+ $redirectUrl = $request->getUrl()->resolve($redirectUrl);\r
+ }\r
+ $redirect = clone $request;\r
+ $redirect->setUrl($redirectUrl);\r
+ if (303 == $response->getStatus() || (!$request->getConfig('strict_redirects')\r
+ && in_array($response->getStatus(), array(301, 302)))\r
+ ) {\r
+ $redirect->setMethod(HTTP_Request2::METHOD_GET);\r
+ $redirect->setBody('');\r
+ }\r
+\r
+ if (0 < $this->redirectCountdown) {\r
+ $this->redirectCountdown--;\r
+ }\r
+ return $this->sendRequest($redirect);\r
+ }\r
+\r
/**\r
* Checks whether another request should be performed with server digest auth\r
*\r
* - auth credentials should be set in the request object\r
* - response should contain WWW-Authenticate header with digest challenge\r
* - there is either no challenge stored for this URL or new challenge\r
- * contains stale=true parameter (in other case we probably just failed \r
+ * contains stale=true parameter (in other case we probably just failed\r
* due to invalid username / password)\r
*\r
* The method stores challenge values in $challenges static property\r
* - proxy auth credentials should be set in the request object\r
* - response should contain Proxy-Authenticate header with digest challenge\r
* - there is either no challenge stored for this proxy or new challenge\r
- * contains stale=true parameter (in other case we probably just failed \r
+ * contains stale=true parameter (in other case we probably just failed\r
* due to invalid username / password)\r
*\r
* The method stores challenge values in $challenges static property\r
* Extracts digest method challenge from (WWW|Proxy)-Authenticate header value\r
*\r
* There is a problem with implementation of RFC 2617: several of the parameters\r
- * here are defined as quoted-string and thus may contain backslash escaped\r
+ * are defined as quoted-string there and thus may contain backslash escaped\r
* double quotes (RFC 2616, section 2.2). However, RFC 2617 defines unq(X) as\r
* just value of quoted-string X without surrounding quotes, it doesn't speak\r
* about removing backslash escaping.\r
* - Squid allows (manually escaped) quotes there, but it is impossible to\r
* authorize with either escaped or unescaped quotes used in digest,\r
* probably it can't parse the response (?)\r
- * - Both IE and Firefox display realm value with backslashes in \r
+ * - Both IE and Firefox display realm value with backslashes in\r
* the password popup and apparently use the same value for digest\r
*\r
* HTTP_Request2 follows IE and Firefox (and hopefully RFC 2617) in\r
- * quoted-string handling, unfortunately that means failure to authorize \r
+ * quoted-string handling, unfortunately that means failure to authorize\r
* sometimes\r
*\r
* @param string value of WWW-Authenticate or Proxy-Authenticate header\r
* @return mixed associative array with challenge parameters, false if\r
* no challenge is present in header value\r
- * @throws HTTP_Request2_Exception in case of unsupported challenge parameters\r
+ * @throws HTTP_Request2_NotImplementedException in case of unsupported challenge parameters\r
*/\r
protected function parseDigestChallenge($headerValue)\r
{\r
}\r
}\r
// we only support qop=auth\r
- if (!empty($paramsAry['qop']) && \r
+ if (!empty($paramsAry['qop']) &&\r
!in_array('auth', array_map('trim', explode(',', $paramsAry['qop'])))\r
) {\r
- throw new HTTP_Request2_Exception(\r
+ throw new HTTP_Request2_NotImplementedException(\r
"Only 'auth' qop is currently supported in digest authentication, " .\r
"server requested '{$paramsAry['qop']}'"\r
);\r
}\r
// we only support algorithm=MD5\r
if (!empty($paramsAry['algorithm']) && 'MD5' != $paramsAry['algorithm']) {\r
- throw new HTTP_Request2_Exception(\r
+ throw new HTTP_Request2_NotImplementedException(\r
"Only 'MD5' algorithm is currently supported in digest authentication, " .\r
"server requested '{$paramsAry['algorithm']}'"\r
);\r
}\r
\r
- return $paramsAry; \r
+ return $paramsAry;\r
}\r
\r
/**\r
* @param array challenge to update\r
* @param string value of [Proxy-]Authentication-Info header\r
* @todo validate server rspauth response\r
- */ \r
+ */\r
protected function updateChallenge(&$challenge, $headerValue)\r
{\r
$authParam = '!(' . self::REGEXP_TOKEN . ')\\s*=\\s*(' .\r
* @param array digest challenge parameters\r
* @return string value of [Proxy-]Authorization request header\r
* @link http://tools.ietf.org/html/rfc2617#section-3.2.2\r
- */ \r
+ */\r
protected function createDigestResponse($user, $password, $url, &$challenge)\r
{\r
- if (false !== ($q = strpos($url, '?')) && \r
+ if (false !== ($q = strpos($url, '?')) &&\r
$this->request->getConfig('digest_compat_ie')\r
) {\r
$url = substr($url, 0, $q);\r
'nonce="' . $challenge['nonce'] . '", ' .\r
'uri="' . $url . '", ' .\r
'response="' . $digest . '"' .\r
- (!empty($challenge['opaque'])? \r
+ (!empty($challenge['opaque'])?\r
', opaque="' . $challenge['opaque'] . '"':\r
'') .\r
(!empty($challenge['qop'])?\r
* @param array request headers\r
* @param string request host (needed for digest authentication)\r
* @param string request URL (needed for digest authentication)\r
- * @throws HTTP_Request2_Exception\r
+ * @throws HTTP_Request2_NotImplementedException\r
*/\r
protected function addAuthorizationHeader(&$headers, $requestHost, $requestUrl)\r
{\r
}\r
switch ($auth['scheme']) {\r
case HTTP_Request2::AUTH_BASIC:\r
- $headers['authorization'] = \r
+ $headers['authorization'] =\r
'Basic ' . base64_encode($auth['user'] . ':' . $auth['password']);\r
break;\r
\r
foreach (array_keys(self::$challenges) as $key) {\r
if ($key == substr($fullUrl, 0, strlen($key))) {\r
$headers['authorization'] = $this->createDigestResponse(\r
- $auth['user'], $auth['password'], \r
+ $auth['user'], $auth['password'],\r
$requestUrl, self::$challenges[$key]\r
);\r
$this->serverChallenge =& self::$challenges[$key];\r
break;\r
\r
default:\r
- throw new HTTP_Request2_Exception(\r
+ throw new HTTP_Request2_NotImplementedException(\r
"Unknown HTTP authentication scheme '{$auth['scheme']}'"\r
);\r
}\r
*\r
* @param array request headers\r
* @param string request URL (needed for digest authentication)\r
- * @throws HTTP_Request2_Exception\r
+ * @throws HTTP_Request2_NotImplementedException\r
*/\r
protected function addProxyAuthorizationHeader(&$headers, $requestUrl)\r
{\r
break;\r
\r
default:\r
- throw new HTTP_Request2_Exception(\r
+ throw new HTTP_Request2_NotImplementedException(\r
"Unknown HTTP authentication scheme '" .\r
$this->request->getConfig('proxy_auth_scheme') . "'"\r
);\r
) {\r
$headers['accept-encoding'] = 'gzip, deflate';\r
}\r
+ if (($jar = $this->request->getCookieJar())\r
+ && ($cookies = $jar->getMatching($this->request->getUrl(), true))\r
+ ) {\r
+ $headers['cookie'] = (empty($headers['cookie'])? '': $headers['cookie'] . '; ') . $cookies;\r
+ }\r
\r
$this->addAuthorizationHeader($headers, $host, $requestUrl);\r
$this->addProxyAuthorizationHeader($headers, $requestUrl);\r
/**\r
* Sends the request body\r
*\r
- * @throws HTTP_Request2_Exception\r
+ * @throws HTTP_Request2_MessageException\r
*/\r
protected function writeBody()\r
{\r
$str = $this->requestBody->read($bufferSize);\r
}\r
if (false === @fwrite($this->socket, $str, strlen($str))) {\r
- throw new HTTP_Request2_Exception('Error writing request');\r
+ throw new HTTP_Request2_MessageException('Error writing request');\r
}\r
// Provide the length of written string to the observer, request #7630\r
$this->request->setLastEvent('sentBodyPart', strlen($str));\r
- $position += strlen($str); \r
+ $position += strlen($str);\r
}\r
+ $this->request->setLastEvent('sentBody', $this->contentLength);\r
}\r
\r
/**\r
$bufferSize = $this->request->getConfig('buffer_size');\r
\r
do {\r
- $response = new HTTP_Request2_Response($this->readLine($bufferSize), true);\r
+ $response = new HTTP_Request2_Response(\r
+ $this->readLine($bufferSize), true, $this->request->getUrl()\r
+ );\r
do {\r
$headerLine = $this->readLine($bufferSize);\r
$response->parseHeaderLine($headerLine);\r
}\r
\r
/**\r
- * Reads until either the end of the socket or a newline, whichever comes first \r
+ * Reads until either the end of the socket or a newline, whichever comes first\r
*\r
- * Strips the trailing newline from the returned data, handles global \r
- * request timeout. Method idea borrowed from Net_Socket PEAR package. \r
+ * Strips the trailing newline from the returned data, handles global\r
+ * request timeout. Method idea borrowed from Net_Socket PEAR package.\r
*\r
* @param int buffer size to use for reading\r
* @return Available data up to the newline (not including newline)\r
- * @throws HTTP_Request2_Exception In case of timeout\r
+ * @throws HTTP_Request2_MessageException In case of timeout\r
*/\r
protected function readLine($bufferSize)\r
{\r
$line = '';\r
while (!feof($this->socket)) {\r
- if ($this->timeout) {\r
- stream_set_timeout($this->socket, max($this->timeout - time(), 1));\r
+ if ($this->deadline) {\r
+ stream_set_timeout($this->socket, max($this->deadline - time(), 1));\r
}\r
$line .= @fgets($this->socket, $bufferSize);\r
$info = stream_get_meta_data($this->socket);\r
- if ($info['timed_out'] || $this->timeout && time() > $this->timeout) {\r
- throw new HTTP_Request2_Exception(\r
- 'Request timed out after ' . \r
- $this->request->getConfig('timeout') . ' second(s)'\r
+ if ($info['timed_out'] || $this->deadline && time() > $this->deadline) {\r
+ $reason = $this->deadline\r
+ ? 'after ' . $this->request->getConfig('timeout') . ' second(s)'\r
+ : 'due to default_socket_timeout php.ini setting';\r
+ throw new HTTP_Request2_MessageException(\r
+ "Request timed out {$reason}", HTTP_Request2_Exception::TIMEOUT\r
);\r
}\r
if (substr($line, -1) == "\n") {\r
*\r
* @param int Reads up to this number of bytes\r
* @return Data read from socket\r
- * @throws HTTP_Request2_Exception In case of timeout\r
+ * @throws HTTP_Request2_MessageException In case of timeout\r
*/\r
protected function fread($length)\r
{\r
- if ($this->timeout) {\r
- stream_set_timeout($this->socket, max($this->timeout - time(), 1));\r
+ if ($this->deadline) {\r
+ stream_set_timeout($this->socket, max($this->deadline - time(), 1));\r
}\r
$data = fread($this->socket, $length);\r
$info = stream_get_meta_data($this->socket);\r
- if ($info['timed_out'] || $this->timeout && time() > $this->timeout) {\r
- throw new HTTP_Request2_Exception(\r
- 'Request timed out after ' . \r
- $this->request->getConfig('timeout') . ' second(s)'\r
+ if ($info['timed_out'] || $this->deadline && time() > $this->deadline) {\r
+ $reason = $this->deadline\r
+ ? 'after ' . $this->request->getConfig('timeout') . ' second(s)'\r
+ : 'due to default_socket_timeout php.ini setting';\r
+ throw new HTTP_Request2_MessageException(\r
+ "Request timed out {$reason}", HTTP_Request2_Exception::TIMEOUT\r
);\r
}\r
return $data;\r
*\r
* @param int buffer size to use for reading\r
* @return string\r
- * @throws HTTP_Request2_Exception\r
+ * @throws HTTP_Request2_MessageException\r
*/\r
protected function readChunked($bufferSize)\r
{\r
if (0 == $this->chunkLength) {\r
$line = $this->readLine($bufferSize);\r
if (!preg_match('/^([0-9a-f]+)/i', $line, $matches)) {\r
- throw new HTTP_Request2_Exception(\r
- "Cannot decode chunked response, invalid chunk length '{$line}'"\r
+ throw new HTTP_Request2_MessageException(\r
+ "Cannot decode chunked response, invalid chunk length '{$line}'",\r
+ HTTP_Request2_Exception::DECODE_ERROR\r
);\r
} else {\r
$this->chunkLength = hexdec($matches[1]);\r