GuzzleHttpHandlerStreamHandler

HTTP handler that uses PHP's HTTP stream wrapper.

Defined (1)

The class is defined in the following location(s).

/lib/Azure/GuzzleHttp/Handler/StreamHandler.php  
  1. class StreamHandler 
  2. private $lastHeaders = []; 
  3.  
  4. /** 
  5. * Sends an HTTP request. 
  6. * @param RequestInterface $request Request to send. 
  7. * @param array $options Request transfer options. 
  8. * @return PromiseInterface 
  9. */ 
  10. public function __invoke(RequestInterface $request, array $options) 
  11. // Sleep if there is a delay specified. 
  12. if (isset($options['delay'])) { 
  13. usleep($options['delay'] * 1000); 
  14.  
  15. $startTime = isset($options['on_stats']) ? microtime(true) : null; 
  16.  
  17. try { 
  18. // Does not support the expect header. 
  19. $request = $request->withoutHeader('Expect'); 
  20.  
  21. // Append a content-length header if body size is zero to match 
  22. // cURL's behavior. 
  23. if (0 === $request->getBody()->getSize()) { 
  24. $request = $request->withHeader('Content-Length', 0); 
  25.  
  26. return $this->createResponse( 
  27. $request,  
  28. $options,  
  29. $this->createStream($request, $options),  
  30. $startTime 
  31. ); 
  32. } catch (\InvalidArgumentException $e) { 
  33. throw $e; 
  34. } catch (\Exception $e) { 
  35. // Determine if the error was a networking error. 
  36. $message = $e->getMessage(); 
  37. // This list can probably get more comprehensive. 
  38. if (strpos($message, 'getaddrinfo') // DNS lookup failed 
  39. || strpos($message, 'Connection refused') 
  40. || strpos($message, "couldn't connect to host") // error on HHVM 
  41. ) { 
  42. $e = new ConnectException($e->getMessage(), $request, $e); 
  43. $e = RequestException::wrapException($request, $e); 
  44. $this->invokeStats($options, $request, $startTime, null, $e); 
  45.  
  46. return new RejectedPromise($e); 
  47.  
  48. private function invokeStats( 
  49. array $options,  
  50. RequestInterface $request,  
  51. $startTime,  
  52. ResponseInterface $response = null,  
  53. $error = null 
  54. ) { 
  55. if (isset($options['on_stats'])) { 
  56. $stats = new TransferStats( 
  57. $request,  
  58. $response,  
  59. microtime(true) - $startTime,  
  60. $error,  
  61. [] 
  62. ); 
  63. call_user_func($options['on_stats'], $stats); 
  64.  
  65. private function createResponse( 
  66. RequestInterface $request,  
  67. array $options,  
  68. $stream,  
  69. $startTime 
  70. ) { 
  71. $hdrs = $this->lastHeaders; 
  72. $this->lastHeaders = []; 
  73. $parts = explode(' ', array_shift($hdrs), 3); 
  74. $ver = explode('/', $parts[0])[1]; 
  75. $status = $parts[1]; 
  76. $reason = isset($parts[2]) ? $parts[2] : null; 
  77. $headers = \GuzzleHttp\headers_from_lines($hdrs); 
  78. list ($stream, $headers) = $this->checkDecode($options, $headers, $stream); 
  79. $stream = Psr7\stream_for($stream); 
  80. $sink = $stream; 
  81.  
  82. if (strcasecmp('HEAD', $request->getMethod())) { 
  83. $sink = $this->createSink($stream, $options); 
  84.  
  85. $response = new Psr7\Response($status, $headers, $sink, $ver, $reason); 
  86.  
  87. if (isset($options['on_headers'])) { 
  88. try { 
  89. $options['on_headers']($response); 
  90. } catch (\Exception $e) { 
  91. $msg = 'An error was encountered during the on_headers event'; 
  92. $ex = new RequestException($msg, $request, $response, $e); 
  93. return new RejectedPromise($ex); 
  94.  
  95. // Do not drain when the request is a HEAD request because they have 
  96. // no body. 
  97. if ($sink !== $stream) { 
  98. $this->drain( 
  99. $stream,  
  100. $sink,  
  101. $response->getHeaderLine('Content-Length') 
  102. ); 
  103.  
  104. $this->invokeStats($options, $request, $startTime, $response, null); 
  105.  
  106. return new FulfilledPromise($response); 
  107.  
  108. private function createSink(StreamInterface $stream, array $options) 
  109. if (!empty($options['stream'])) { 
  110. return $stream; 
  111.  
  112. $sink = isset($options['sink']) 
  113. ? $options['sink'] 
  114. : fopen('php://temp', 'r+'); 
  115.  
  116. return is_string($sink) 
  117. ? new Psr7\LazyOpenStream($sink, 'w+') 
  118. : Psr7\stream_for($sink); 
  119.  
  120. private function checkDecode(array $options, array $headers, $stream) 
  121. // Automatically decode responses when instructed. 
  122. if (!empty($options['decode_content'])) { 
  123. $normalizedKeys = \GuzzleHttp\normalize_header_keys($headers); 
  124. if (isset($normalizedKeys['content-encoding'])) { 
  125. $encoding = $headers[$normalizedKeys['content-encoding']]; 
  126. if ($encoding[0] === 'gzip' || $encoding[0] === 'deflate') { 
  127. $stream = new Psr7\InflateStream( 
  128. Psr7\stream_for($stream) 
  129. ); 
  130. $headers['x-encoded-content-encoding'] 
  131. = $headers[$normalizedKeys['content-encoding']]; 
  132. // Remove content-encoding header 
  133. unset($headers[$normalizedKeys['content-encoding']]); 
  134. // Fix content-length header 
  135. if (isset($normalizedKeys['content-length'])) { 
  136. $headers['x-encoded-content-length'] 
  137. = $headers[$normalizedKeys['content-length']]; 
  138.  
  139. $length = (int) $stream->getSize(); 
  140. if ($length === 0) { 
  141. unset($headers[$normalizedKeys['content-length']]); 
  142. } else { 
  143. $headers[$normalizedKeys['content-length']] = [$length]; 
  144.  
  145. return [$stream, $headers]; 
  146.  
  147. /** 
  148. * Drains the source stream into the "sink" client option. 
  149. * @param StreamInterface $source 
  150. * @param StreamInterface $sink 
  151. * @param string $contentLength Header specifying the amount of 
  152. * data to read. 
  153. * @return StreamInterface 
  154. * @throws \RuntimeException when the sink option is invalid. 
  155. */ 
  156. private function drain( 
  157. StreamInterface $source,  
  158. StreamInterface $sink,  
  159. $contentLength 
  160. ) { 
  161. // If a content-length header is provided, then stop reading once 
  162. // that number of bytes has been read. This can prevent infinitely 
  163. // reading from a stream when dealing with servers that do not honor 
  164. // Connection: Close headers. 
  165. Psr7\copy_to_stream( 
  166. $source,  
  167. $sink,  
  168. (strlen($contentLength) > 0 && (int) $contentLength > 0) ? (int) $contentLength : -1 
  169. ); 
  170.  
  171. $sink->seek(0); 
  172. $source->close(); 
  173.  
  174. return $sink; 
  175.  
  176. /** 
  177. * Create a resource and check to ensure it was created successfully 
  178. * @param callable $callback Callable that returns stream resource 
  179. * @return resource 
  180. * @throws \RuntimeException on error 
  181. */ 
  182. private function createResource(callable $callback) 
  183. $errors = null; 
  184. set_error_handler(function ($_, $msg, $file, $line) use (&$errors) { 
  185. $errors[] = [ 
  186. 'message' => $msg,  
  187. 'file' => $file,  
  188. 'line' => $line 
  189. ]; 
  190. return true; 
  191. }); 
  192.  
  193. $resource = $callback(); 
  194. restore_error_handler(); 
  195.  
  196. if (!$resource) { 
  197. $message = 'Error creating resource: '; 
  198. foreach ($errors as $err) { 
  199. foreach ($err as $key => $value) { 
  200. $message .= "[$key] $value" . PHP_EOL; 
  201. throw new \RuntimeException(trim($message)); 
  202.  
  203. return $resource; 
  204.  
  205. private function createStream(RequestInterface $request, array $options) 
  206. static $methods; 
  207. if (!$methods) { 
  208. $methods = array_flip(get_class_methods(__CLASS__)); 
  209.  
  210. // HTTP/1.1 streams using the PHP stream wrapper require a 
  211. // Connection: close header 
  212. if ($request->getProtocolVersion() == '1.1' 
  213. && !$request->hasHeader('Connection') 
  214. ) { 
  215. $request = $request->withHeader('Connection', 'close'); 
  216.  
  217. // Ensure SSL is verified by default 
  218. if (!isset($options['verify'])) { 
  219. $options['verify'] = true; 
  220.  
  221. $params = []; 
  222. $context = $this->getDefaultContext($request, $options); 
  223.  
  224. if (isset($options['on_headers']) && !is_callable($options['on_headers'])) { 
  225. throw new \InvalidArgumentException('on_headers must be callable'); 
  226.  
  227. if (!empty($options)) { 
  228. foreach ($options as $key => $value) { 
  229. $method = "add_{$key}"; 
  230. if (isset($methods[$method])) { 
  231. $this->{$method}($request, $context, $value, $params); 
  232.  
  233. if (isset($options['stream_context'])) { 
  234. if (!is_array($options['stream_context'])) { 
  235. throw new \InvalidArgumentException('stream_context must be an array'); 
  236. $context = array_replace_recursive( 
  237. $context,  
  238. $options['stream_context'] 
  239. ); 
  240.  
  241. $context = $this->createResource( 
  242. function () use ($context, $params) { 
  243. return stream_context_create($context, $params); 
  244. ); 
  245.  
  246. return $this->createResource( 
  247. function () use ($request, &$http_response_header, $context) { 
  248. $resource = fopen((string) $request->getUri()->withFragment(''), 'r', null, $context); 
  249. $this->lastHeaders = $http_response_header; 
  250. return $resource; 
  251. ); 
  252.  
  253. private function getDefaultContext(RequestInterface $request) 
  254. $headers = ''; 
  255. foreach ($request->getHeaders() as $name => $value) { 
  256. foreach ($value as $val) { 
  257. $headers .= "$name: $val\r\n"; 
  258.  
  259. $context = [ 
  260. 'http' => [ 
  261. 'method' => $request->getMethod(),  
  262. 'header' => $headers,  
  263. 'protocol_version' => $request->getProtocolVersion(),  
  264. 'ignore_errors' => true,  
  265. 'follow_location' => 0,  
  266. ],  
  267. ]; 
  268.  
  269. $body = (string) $request->getBody(); 
  270.  
  271. if (!empty($body)) { 
  272. $context['http']['content'] = $body; 
  273. // Prevent the HTTP handler from adding a Content-Type header. 
  274. if (!$request->hasHeader('Content-Type')) { 
  275. $context['http']['header'] .= "Content-Type:\r\n"; 
  276.  
  277. $context['http']['header'] = rtrim($context['http']['header']); 
  278.  
  279. return $context; 
  280.  
  281. private function add_proxy(RequestInterface $request, &$options, $value, &$params) 
  282. if (!is_array($value)) { 
  283. $options['http']['proxy'] = $value; 
  284. } else { 
  285. $scheme = $request->getUri()->getScheme(); 
  286. if (isset($value[$scheme])) { 
  287. if (!isset($value['no']) 
  288. || !\GuzzleHttp\is_host_in_noproxy( 
  289. $request->getUri()->getHost(),  
  290. $value['no'] 
  291. ) { 
  292. $options['http']['proxy'] = $value[$scheme]; 
  293.  
  294. private function add_timeout(RequestInterface $request, &$options, $value, &$params) 
  295. if ($value > 0) { 
  296. $options['http']['timeout'] = $value; 
  297.  
  298. private function add_verify(RequestInterface $request, &$options, $value, &$params) 
  299. if ($value === true) { 
  300. // PHP 5.6 or greater will find the system cert by default. When 
  301. // < 5.6, use the Guzzle bundled cacert. 
  302. if (PHP_VERSION_ID < 50600) { 
  303. $options['ssl']['cafile'] = \GuzzleHttp\default_ca_bundle(); 
  304. } elseif (is_string($value)) { 
  305. $options['ssl']['cafile'] = $value; 
  306. if (!file_exists($value)) { 
  307. throw new \RuntimeException("SSL CA bundle not found: $value"); 
  308. } elseif ($value === false) { 
  309. $options['ssl']['verify_peer'] = false; 
  310. $options['ssl']['verify_peer_name'] = false; 
  311. return; 
  312. } else { 
  313. throw new \InvalidArgumentException('Invalid verify request option'); 
  314.  
  315. $options['ssl']['verify_peer'] = true; 
  316. $options['ssl']['verify_peer_name'] = true; 
  317. $options['ssl']['allow_self_signed'] = false; 
  318.  
  319. private function add_cert(RequestInterface $request, &$options, $value, &$params) 
  320. if (is_array($value)) { 
  321. $options['ssl']['passphrase'] = $value[1]; 
  322. $value = $value[0]; 
  323.  
  324. if (!file_exists($value)) { 
  325. throw new \RuntimeException("SSL certificate not found: {$value}"); 
  326.  
  327. $options['ssl']['local_cert'] = $value; 
  328.  
  329. private function add_progress(RequestInterface $request, &$options, $value, &$params) 
  330. $this->addNotification( 
  331. $params,  
  332. function ($code, $a, $b, $c, $transferred, $total) use ($value) { 
  333. if ($code == STREAM_NOTIFY_PROGRESS) { 
  334. $value($total, $transferred, null, null); 
  335. ); 
  336.  
  337. private function add_debug(RequestInterface $request, &$options, $value, &$params) 
  338. if ($value === false) { 
  339. return; 
  340.  
  341. static $map = [ 
  342. STREAM_NOTIFY_CONNECT => 'CONNECT',  
  343. STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED',  
  344. STREAM_NOTIFY_AUTH_RESULT => 'AUTH_RESULT',  
  345. STREAM_NOTIFY_MIME_TYPE_IS => 'MIME_TYPE_IS',  
  346. STREAM_NOTIFY_FILE_SIZE_IS => 'FILE_SIZE_IS',  
  347. STREAM_NOTIFY_REDIRECTED => 'REDIRECTED',  
  348. STREAM_NOTIFY_PROGRESS => 'PROGRESS',  
  349. STREAM_NOTIFY_FAILURE => 'FAILURE',  
  350. STREAM_NOTIFY_COMPLETED => 'COMPLETED',  
  351. STREAM_NOTIFY_RESOLVE => 'RESOLVE',  
  352. ]; 
  353. static $args = ['severity', 'message', 'message_code',  
  354. 'bytes_transferred', 'bytes_max']; 
  355.  
  356. $value = \GuzzleHttp\debug_resource($value); 
  357. $ident = $request->getMethod() . ' ' . $request->getUri()->withFragment(''); 
  358. $this->addNotification( 
  359. $params,  
  360. function () use ($ident, $value, $map, $args) { 
  361. $passed = func_get_args(); 
  362. $code = array_shift($passed); 
  363. fprintf($value, '<%s> [%s] ', $ident, $map[$code]); 
  364. foreach (array_filter($passed) as $i => $v) { 
  365. fwrite($value, $args[$i] . ': "' . $v . '" '); 
  366. fwrite($value, "\n"); 
  367. ); 
  368.  
  369. private function addNotification(array &$params, callable $notify) 
  370. // Wrap the existing function if needed. 
  371. if (!isset($params['notification'])) { 
  372. $params['notification'] = $notify; 
  373. } else { 
  374. $params['notification'] = $this->callArray([ 
  375. $params['notification'],  
  376. $notify 
  377. ]); 
  378.  
  379. private function callArray(array $functions) 
  380. return function () use ($functions) { 
  381. $args = func_get_args(); 
  382. foreach ($functions as $fn) { 
  383. call_user_func_array($fn, $args); 
  384. };