1<?php 2/** 3 * Elasticsearch PHP client 4 * 5 * @link https://github.com/elastic/elasticsearch-php/ 6 * @copyright Copyright (c) Elasticsearch B.V (https://www.elastic.co) 7 * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0 8 * @license https://www.gnu.org/licenses/lgpl-2.1.html GNU Lesser General Public License, Version 2.1 9 * 10 * Licensed to Elasticsearch B.V under one or more agreements. 11 * Elasticsearch B.V licenses this file to you under the Apache 2.0 License or 12 * the GNU Lesser General Public License, Version 2.1, at your option. 13 * See the LICENSE file in the project root for more information. 14 */ 15 16 17declare(strict_types = 1); 18 19namespace Elasticsearch\Connections; 20 21use Elasticsearch\Client; 22use Elasticsearch\Common\Exceptions\BadRequest400Exception; 23use Elasticsearch\Common\Exceptions\Conflict409Exception; 24use Elasticsearch\Common\Exceptions\Curl\CouldNotConnectToHost; 25use Elasticsearch\Common\Exceptions\Curl\CouldNotResolveHostException; 26use Elasticsearch\Common\Exceptions\Curl\OperationTimeoutException; 27use Elasticsearch\Common\Exceptions\ElasticsearchException; 28use Elasticsearch\Common\Exceptions\Forbidden403Exception; 29use Elasticsearch\Common\Exceptions\MaxRetriesException; 30use Elasticsearch\Common\Exceptions\Missing404Exception; 31use Elasticsearch\Common\Exceptions\NoDocumentsToGetException; 32use Elasticsearch\Common\Exceptions\NoShardAvailableException; 33use Elasticsearch\Common\Exceptions\RequestTimeout408Exception; 34use Elasticsearch\Common\Exceptions\RoutingMissingException; 35use Elasticsearch\Common\Exceptions\ScriptLangNotSupportedException; 36use Elasticsearch\Common\Exceptions\ServerErrorResponseException; 37use Elasticsearch\Common\Exceptions\TransportException; 38use Elasticsearch\Common\Exceptions\Unauthorized401Exception; 39use Elasticsearch\Serializers\SerializerInterface; 40use Elasticsearch\Transport; 41use Exception; 42use GuzzleHttp\Ring\Core; 43use GuzzleHttp\Ring\Exception\ConnectException; 44use GuzzleHttp\Ring\Exception\RingException; 45use Psr\Log\LoggerInterface; 46 47class Connection implements ConnectionInterface 48{ 49 /** 50 * @var callable 51 */ 52 protected $handler; 53 54 /** 55 * @var SerializerInterface 56 */ 57 protected $serializer; 58 59 /** 60 * @var string 61 */ 62 protected $transportSchema = 'http'; // TODO depreciate this default 63 64 /** 65 * @var string 66 */ 67 protected $host; 68 69 /** 70 * @var string|null 71 */ 72 protected $path; 73 74 /** 75 * @var int 76 */ 77 protected $port; 78 79 /** 80 * @var LoggerInterface 81 */ 82 protected $log; 83 84 /** 85 * @var LoggerInterface 86 */ 87 protected $trace; 88 89 /** 90 * @var array 91 */ 92 protected $connectionParams; 93 94 /** 95 * @var array 96 */ 97 protected $headers = []; 98 99 /** 100 * @var bool 101 */ 102 protected $isAlive = false; 103 104 /** 105 * @var float 106 */ 107 private $pingTimeout = 1; //TODO expose this 108 109 /** 110 * @var int 111 */ 112 private $lastPing = 0; 113 114 /** 115 * @var int 116 */ 117 private $failedPings = 0; 118 119 /** 120 * @var mixed[] 121 */ 122 private $lastRequest = array(); 123 124 /** 125 * @var string 126 */ 127 private $OSVersion = null; 128 129 public function __construct( 130 callable $handler, 131 array $hostDetails, 132 array $connectionParams, 133 SerializerInterface $serializer, 134 LoggerInterface $log, 135 LoggerInterface $trace 136 ) { 137 138 if (isset($hostDetails['port']) !== true) { 139 $hostDetails['port'] = 9200; 140 } 141 142 if (isset($hostDetails['scheme'])) { 143 $this->transportSchema = $hostDetails['scheme']; 144 } 145 146 // Only Set the Basic if API Key is not set and setBasicAuthentication was not called prior 147 if (isset($connectionParams['client']['headers']['Authorization']) === false 148 && isset($connectionParams['client']['curl'][CURLOPT_HTTPAUTH]) === false 149 && isset($hostDetails['user']) 150 && isset($hostDetails['pass']) 151 ) { 152 $connectionParams['client']['curl'][CURLOPT_HTTPAUTH] = CURLAUTH_BASIC; 153 $connectionParams['client']['curl'][CURLOPT_USERPWD] = $hostDetails['user'].':'.$hostDetails['pass']; 154 } 155 156 $connectionParams['client']['curl'][CURLOPT_PORT] = $hostDetails['port']; 157 158 if (isset($connectionParams['client']['headers'])) { 159 $this->headers = $connectionParams['client']['headers']; 160 unset($connectionParams['client']['headers']); 161 } 162 163 // Add the User-Agent using the format: <client-repo-name>/<client-version> (metadata-values) 164 $this->headers['User-Agent'] = [sprintf( 165 "elasticsearch-php/%s (%s %s; PHP %s)", 166 Client::VERSION, 167 PHP_OS, 168 $this->getOSVersion(), 169 phpversion() 170 )]; 171 172 // Add x-elastic-client-meta header, if enabled 173 if (isset($connectionParams['client']['x-elastic-client-meta']) && $connectionParams['client']['x-elastic-client-meta']) { 174 $this->headers['x-elastic-client-meta'] = [$this->getElasticMetaHeader($connectionParams)]; 175 } 176 177 $host = $hostDetails['host']; 178 $path = null; 179 if (isset($hostDetails['path']) === true) { 180 $path = $hostDetails['path']; 181 } 182 $port = $hostDetails['port']; 183 184 $this->host = $host; 185 $this->path = $path; 186 $this->port = $port; 187 $this->log = $log; 188 $this->trace = $trace; 189 $this->connectionParams = $connectionParams; 190 $this->serializer = $serializer; 191 192 $this->handler = $this->wrapHandler($handler); 193 } 194 195 /** 196 * @param string $method 197 * @param string $uri 198 * @param null|array $params 199 * @param null $body 200 * @param array $options 201 * @param Transport $transport 202 * @return mixed 203 */ 204 public function performRequest(string $method, string $uri, ?array $params = [], $body = null, array $options = [], Transport $transport = null) 205 { 206 if ($body !== null) { 207 $body = $this->serializer->serialize($body); 208 } 209 210 $headers = $this->headers; 211 if (isset($options['client']['headers']) && is_array($options['client']['headers'])) { 212 $headers = array_merge($this->headers, $options['client']['headers']); 213 } 214 215 $host = $this->host; 216 if (isset($this->connectionParams['client']['port_in_header']) && $this->connectionParams['client']['port_in_header']) { 217 $host .= ':' . $this->port; 218 } 219 220 $request = [ 221 'http_method' => $method, 222 'scheme' => $this->transportSchema, 223 'uri' => $this->getURI($uri, $params), 224 'body' => $body, 225 'headers' => array_merge( 226 [ 227 'Host' => [$host] 228 ], 229 $headers 230 ) 231 ]; 232 233 $request = array_replace_recursive($request, $this->connectionParams, $options); 234 235 // RingPHP does not like if client is empty 236 if (empty($request['client'])) { 237 unset($request['client']); 238 } 239 240 $handler = $this->handler; 241 $future = $handler($request, $this, $transport, $options); 242 243 return $future; 244 } 245 246 public function getTransportSchema(): string 247 { 248 return $this->transportSchema; 249 } 250 251 public function getLastRequestInfo(): array 252 { 253 return $this->lastRequest; 254 } 255 256 private function wrapHandler(callable $handler): callable 257 { 258 return function (array $request, Connection $connection, Transport $transport = null, $options) use ($handler) { 259 260 $this->lastRequest = []; 261 $this->lastRequest['request'] = $request; 262 263 // Send the request using the wrapped handler. 264 $response = Core::proxy( 265 $handler($request), 266 function ($response) use ($connection, $transport, $request, $options) { 267 268 $this->lastRequest['response'] = $response; 269 270 if (isset($response['error']) === true) { 271 if ($response['error'] instanceof ConnectException || $response['error'] instanceof RingException) { 272 $this->log->warning("Curl exception encountered."); 273 274 $exception = $this->getCurlRetryException($request, $response); 275 276 $this->logRequestFail($request, $response, $exception); 277 278 $node = $connection->getHost(); 279 $this->log->warning("Marking node $node dead."); 280 $connection->markDead(); 281 282 // If the transport has not been set, we are inside a Ping or Sniff, 283 // so we don't want to retrigger retries anyway. 284 // 285 // TODO this could be handled better, but we are limited because connectionpools do not 286 // have access to Transport. Architecturally, all of this needs to be refactored 287 if (isset($transport) === true) { 288 $transport->connectionPool->scheduleCheck(); 289 290 $neverRetry = isset($request['client']['never_retry']) ? $request['client']['never_retry'] : false; 291 $shouldRetry = $transport->shouldRetry($request); 292 $shouldRetryText = ($shouldRetry) ? 'true' : 'false'; 293 294 $this->log->warning("Retries left? $shouldRetryText"); 295 if ($shouldRetry && !$neverRetry) { 296 return $transport->performRequest( 297 $request['http_method'], 298 $request['uri'], 299 [], 300 $request['body'], 301 $options 302 ); 303 } 304 } 305 306 $this->log->warning("Out of retries, throwing exception from $node"); 307 // Only throw if we run out of retries 308 throw $exception; 309 } else { 310 // Something went seriously wrong, bail 311 $exception = new TransportException($response['error']->getMessage()); 312 $this->logRequestFail($request, $response, $exception); 313 throw $exception; 314 } 315 } else { 316 $connection->markAlive(); 317 318 if (isset($response['headers']['Warning'])) { 319 $this->logWarning($request, $response); 320 } 321 if (isset($response['body']) === true) { 322 $response['body'] = stream_get_contents($response['body']); 323 $this->lastRequest['response']['body'] = $response['body']; 324 } 325 326 if ($response['status'] >= 400 && $response['status'] < 500) { 327 $ignore = $request['client']['ignore'] ?? []; 328 // Skip 404 if succeeded true in the body (e.g. clear_scroll) 329 $body = $response['body'] ?? ''; 330 if (strpos($body, '"succeeded":true') !== false) { 331 $ignore[] = 404; 332 } 333 $this->process4xxError($request, $response, $ignore); 334 } elseif ($response['status'] >= 500) { 335 $ignore = $request['client']['ignore'] ?? []; 336 $this->process5xxError($request, $response, $ignore); 337 } 338 339 // No error, deserialize 340 $response['body'] = $this->serializer->deserialize($response['body'], $response['transfer_stats']); 341 } 342 $this->logRequestSuccess($request, $response); 343 344 return isset($request['client']['verbose']) && $request['client']['verbose'] === true ? $response : $response['body']; 345 } 346 ); 347 348 return $response; 349 }; 350 } 351 352 private function getURI(string $uri, ?array $params): string 353 { 354 if (isset($params) === true && !empty($params)) { 355 $params = array_map( 356 function ($value) { 357 if ($value === true) { 358 return 'true'; 359 } elseif ($value === false) { 360 return 'false'; 361 } 362 363 return $value; 364 }, 365 $params 366 ); 367 368 $uri .= '?' . http_build_query($params); 369 } 370 371 if ($this->path !== null) { 372 $uri = $this->path . $uri; 373 } 374 375 return $uri ?? ''; 376 } 377 378 public function getHeaders(): array 379 { 380 return $this->headers; 381 } 382 383 public function logWarning(array $request, array $response): void 384 { 385 $this->log->warning('Deprecation', $response['headers']['Warning']); 386 } 387 388 /** 389 * Log a successful request 390 * 391 * @param array $request 392 * @param array $response 393 * @return void 394 */ 395 public function logRequestSuccess(array $request, array $response): void 396 { 397 $port = $request['client']['curl'][CURLOPT_PORT] ?? $response['transfer_stats']['primary_port'] ?? ''; 398 $uri = $this->addPortInUrl($response['effective_url'], (int) $port); 399 400 $this->log->debug('Request Body', array($request['body'])); 401 $this->log->info( 402 'Request Success:', 403 array( 404 'method' => $request['http_method'], 405 'uri' => $uri, 406 'port' => $port, 407 'headers' => $request['headers'], 408 'HTTP code' => $response['status'], 409 'duration' => $response['transfer_stats']['total_time'], 410 ) 411 ); 412 $this->log->debug('Response', array($response['body'])); 413 414 // Build the curl command for Trace. 415 $curlCommand = $this->buildCurlCommand($request['http_method'], $uri, $request['body']); 416 $this->trace->info($curlCommand); 417 $this->trace->debug( 418 'Response:', 419 array( 420 'response' => $response['body'], 421 'method' => $request['http_method'], 422 'uri' => $uri, 423 'port' => $port, 424 'HTTP code' => $response['status'], 425 'duration' => $response['transfer_stats']['total_time'], 426 ) 427 ); 428 } 429 430 /** 431 * Log a failed request 432 * 433 * @param array $request 434 * @param array $response 435 * @param \Exception $exception 436 * 437 * @return void 438 */ 439 public function logRequestFail(array $request, array $response, \Exception $exception): void 440 { 441 $port = $request['client']['curl'][CURLOPT_PORT] ?? $response['transfer_stats']['primary_port'] ?? ''; 442 $uri = $this->addPortInUrl($response['effective_url'], (int) $port); 443 444 $this->log->debug('Request Body', array($request['body'])); 445 $this->log->warning( 446 'Request Failure:', 447 array( 448 'method' => $request['http_method'], 449 'uri' => $uri, 450 'port' => $port, 451 'headers' => $request['headers'], 452 'HTTP code' => $response['status'], 453 'duration' => $response['transfer_stats']['total_time'], 454 'error' => $exception->getMessage(), 455 ) 456 ); 457 $this->log->warning('Response', array($response['body'])); 458 459 // Build the curl command for Trace. 460 $curlCommand = $this->buildCurlCommand($request['http_method'], $uri, $request['body']); 461 $this->trace->info($curlCommand); 462 $this->trace->debug( 463 'Response:', 464 array( 465 'response' => $response, 466 'method' => $request['http_method'], 467 'uri' => $uri, 468 'port' => $port, 469 'HTTP code' => $response['status'], 470 'duration' => $response['transfer_stats']['total_time'], 471 ) 472 ); 473 } 474 475 public function ping(): bool 476 { 477 $options = [ 478 'client' => [ 479 'timeout' => $this->pingTimeout, 480 'never_retry' => true, 481 'verbose' => true 482 ] 483 ]; 484 try { 485 $response = $this->performRequest('HEAD', '/', null, null, $options); 486 $response = $response->wait(); 487 } catch (TransportException $exception) { 488 $this->markDead(); 489 490 return false; 491 } 492 493 if ($response['status'] === 200) { 494 $this->markAlive(); 495 496 return true; 497 } else { 498 $this->markDead(); 499 500 return false; 501 } 502 } 503 504 /** 505 * @return array|\GuzzleHttp\Ring\Future\FutureArray 506 */ 507 public function sniff() 508 { 509 $options = [ 510 'client' => [ 511 'timeout' => $this->pingTimeout, 512 'never_retry' => true 513 ] 514 ]; 515 516 return $this->performRequest('GET', '/_nodes/', null, null, $options); 517 } 518 519 public function isAlive(): bool 520 { 521 return $this->isAlive; 522 } 523 524 public function markAlive(): void 525 { 526 $this->failedPings = 0; 527 $this->isAlive = true; 528 $this->lastPing = time(); 529 } 530 531 public function markDead(): void 532 { 533 $this->isAlive = false; 534 $this->failedPings += 1; 535 $this->lastPing = time(); 536 } 537 538 public function getLastPing(): int 539 { 540 return $this->lastPing; 541 } 542 543 public function getPingFailures(): int 544 { 545 return $this->failedPings; 546 } 547 548 public function getHost(): string 549 { 550 return $this->host; 551 } 552 553 public function getUserPass(): ?string 554 { 555 return $this->connectionParams['client']['curl'][CURLOPT_USERPWD] ?? null; 556 } 557 558 public function getPath(): ?string 559 { 560 return $this->path; 561 } 562 563 /** 564 * @return int 565 */ 566 public function getPort() 567 { 568 return $this->port; 569 } 570 571 protected function getCurlRetryException(array $request, array $response): ElasticsearchException 572 { 573 $exception = null; 574 $message = $response['error']->getMessage(); 575 $exception = new MaxRetriesException($message); 576 switch ($response['curl']['errno']) { 577 case 6: 578 $exception = new CouldNotResolveHostException($message, 0, $exception); 579 break; 580 case 7: 581 $exception = new CouldNotConnectToHost($message, 0, $exception); 582 break; 583 case 28: 584 $exception = new OperationTimeoutException($message, 0, $exception); 585 break; 586 } 587 588 return $exception; 589 } 590 591 /** 592 * Get the x-elastic-client-meta header 593 * 594 * The header format is specified by the following regex: 595 * ^[a-z]{1,}=[a-z0-9\.\-]{1,}(?:,[a-z]{1,}=[a-z0-9\.\-]+)*$ 596 */ 597 private function getElasticMetaHeader(array $connectionParams): string 598 { 599 $phpSemVersion = sprintf("%d.%d.%d", PHP_MAJOR_VERSION, PHP_MINOR_VERSION, PHP_RELEASE_VERSION); 600 // Reduce the size in case of '-snapshot' version 601 $clientVersion = str_replace('-snapshot', '-s', strtolower(Client::VERSION)); 602 $clientMeta = sprintf( 603 "es=%s,php=%s,t=%s,a=%d", 604 $clientVersion, 605 $phpSemVersion, 606 $clientVersion, 607 isset($connectionParams['client']['future']) && $connectionParams['client']['future'] === 'lazy' ? 1 : 0 608 ); 609 if (function_exists('curl_version')) { 610 $curlVersion = curl_version(); 611 if (isset($curlVersion['version'])) { 612 $clientMeta .= sprintf(",cu=%s", $curlVersion['version']); // cu = curl library 613 } 614 } 615 return $clientMeta; 616 } 617 618 /** 619 * Get the OS version using php_uname if available 620 * otherwise it returns an empty string 621 * 622 * @see https://github.com/elastic/elasticsearch-php/issues/922 623 */ 624 private function getOSVersion(): string 625 { 626 if ($this->OSVersion === null) { 627 $this->OSVersion = strpos(strtolower(ini_get('disable_functions')), 'php_uname') !== false 628 ? '' 629 : php_uname("r"); 630 } 631 return $this->OSVersion; 632 } 633 634 /** 635 * Add the port value in the URL if not present 636 */ 637 private function addPortInUrl(string $uri, int $port): string 638 { 639 if (strpos($uri, ':', 7) !== false) { 640 return $uri; 641 } 642 return preg_replace('#([^/])/([^/])#', sprintf("$1:%s/$2", $port), $uri, 1); 643 } 644 645 /** 646 * Construct a string cURL command 647 */ 648 private function buildCurlCommand(string $method, string $url, ?string $body): string 649 { 650 if (strpos($url, '?') === false) { 651 $url .= '?pretty=true'; 652 } else { 653 str_replace('?', '?pretty=true', $url); 654 } 655 656 $curlCommand = 'curl -X' . strtoupper($method); 657 $curlCommand .= " '" . $url . "'"; 658 659 if (isset($body) === true && $body !== '') { 660 $curlCommand .= " -d '" . $body . "'"; 661 } 662 663 return $curlCommand; 664 } 665 666 private function process4xxError(array $request, array $response, array $ignore): ?ElasticsearchException 667 { 668 $statusCode = $response['status']; 669 670 /** 671 * @var \Exception $exception 672*/ 673 $exception = $this->tryDeserialize400Error($response); 674 675 if (array_search($response['status'], $ignore) !== false) { 676 return null; 677 } 678 679 $responseBody = $this->convertBodyToString($response['body'], $statusCode, $exception); 680 if ($statusCode === 401) { 681 $exception = new Unauthorized401Exception($responseBody, $statusCode); 682 } elseif ($statusCode === 403) { 683 $exception = new Forbidden403Exception($responseBody, $statusCode); 684 } elseif ($statusCode === 404) { 685 $exception = new Missing404Exception($responseBody, $statusCode); 686 } elseif ($statusCode === 409) { 687 $exception = new Conflict409Exception($responseBody, $statusCode); 688 } elseif ($statusCode === 400 && strpos($responseBody, 'script_lang not supported') !== false) { 689 $exception = new ScriptLangNotSupportedException($responseBody. $statusCode); 690 } elseif ($statusCode === 408) { 691 $exception = new RequestTimeout408Exception($responseBody, $statusCode); 692 } else { 693 $exception = new BadRequest400Exception($responseBody, $statusCode); 694 } 695 696 $this->logRequestFail($request, $response, $exception); 697 698 throw $exception; 699 } 700 701 private function process5xxError(array $request, array $response, array $ignore): ?ElasticsearchException 702 { 703 $statusCode = (int) $response['status']; 704 $responseBody = $response['body']; 705 706 /** 707 * @var \Exception $exception 708*/ 709 $exception = $this->tryDeserialize500Error($response); 710 711 $exceptionText = "[$statusCode Server Exception] ".$exception->getMessage(); 712 $this->log->error($exceptionText); 713 $this->log->error($exception->getTraceAsString()); 714 715 if (array_search($statusCode, $ignore) !== false) { 716 return null; 717 } 718 719 if ($statusCode === 500 && strpos($responseBody, "RoutingMissingException") !== false) { 720 $exception = new RoutingMissingException($exception->getMessage(), $statusCode, $exception); 721 } elseif ($statusCode === 500 && preg_match('/ActionRequestValidationException.+ no documents to get/', $responseBody) === 1) { 722 $exception = new NoDocumentsToGetException($exception->getMessage(), $statusCode, $exception); 723 } elseif ($statusCode === 500 && strpos($responseBody, 'NoShardAvailableActionException') !== false) { 724 $exception = new NoShardAvailableException($exception->getMessage(), $statusCode, $exception); 725 } else { 726 $exception = new ServerErrorResponseException( 727 $this->convertBodyToString($responseBody, $statusCode, $exception), 728 $statusCode 729 ); 730 } 731 732 $this->logRequestFail($request, $response, $exception); 733 734 throw $exception; 735 } 736 737 private function convertBodyToString($body, int $statusCode, Exception $exception) : string 738 { 739 if (empty($body)) { 740 return sprintf( 741 "Unknown %d error from Elasticsearch %s", 742 $statusCode, 743 $exception->getMessage() 744 ); 745 } 746 // if body is not string, we convert it so it can be used as Exception message 747 if (!is_string($body)) { 748 return json_encode($body); 749 } 750 return $body; 751 } 752 753 private function tryDeserialize400Error(array $response): ElasticsearchException 754 { 755 return $this->tryDeserializeError($response, BadRequest400Exception::class); 756 } 757 758 private function tryDeserialize500Error(array $response): ElasticsearchException 759 { 760 return $this->tryDeserializeError($response, ServerErrorResponseException::class); 761 } 762 763 private function tryDeserializeError(array $response, string $errorClass): ElasticsearchException 764 { 765 $error = $this->serializer->deserialize($response['body'], $response['transfer_stats']); 766 if (is_array($error) === true) { 767 if (isset($error['error']) === false) { 768 // <2.0 "i just blew up" nonstructured exception 769 // $error is an array but we don't know the format, reuse the response body instead 770 // added json_encode to convert into a string 771 return new $errorClass(json_encode($response['body']), (int) $response['status']); 772 } 773 774 // 2.0 structured exceptions 775 if (is_array($error['error']) && array_key_exists('reason', $error['error']) === true) { 776 // Try to use root cause first (only grabs the first root cause) 777 $root = $error['error']['root_cause']; 778 if (isset($root) && isset($root[0])) { 779 $cause = $root[0]['reason']; 780 $type = $root[0]['type']; 781 } else { 782 $cause = $error['error']['reason']; 783 $type = $error['error']['type']; 784 } 785 // added json_encode to convert into a string 786 $original = new $errorClass(json_encode($response['body']), $response['status']); 787 788 return new $errorClass("$type: $cause", (int) $response['status'], $original); 789 } 790 // <2.0 semi-structured exceptions 791 // added json_encode to convert into a string 792 $original = new $errorClass(json_encode($response['body']), $response['status']); 793 794 $errorEncoded = $error['error']; 795 if (is_array($errorEncoded)) { 796 $errorEncoded = json_encode($errorEncoded); 797 } 798 return new $errorClass($errorEncoded, (int) $response['status'], $original); 799 } 800 801 // if responseBody is not string, we convert it so it can be used as Exception message 802 $responseBody = $response['body']; 803 if (!is_string($responseBody)) { 804 $responseBody = json_encode($responseBody); 805 } 806 807 // <2.0 "i just blew up" nonstructured exception 808 return new $errorClass($responseBody); 809 } 810} 811