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