From d30188f178bdbdcbab916bc4f27790932c84ad96 Mon Sep 17 00:00:00 2001 From: Kim Pepper Date: Sat, 18 Jan 2025 00:07:00 +1100 Subject: [PATCH] Handle HTTP exceptions in HttpTransport (#254) * Handle HTTP exceptions in HttpTransport Signed-off-by: Kim Pepper * Generate Client Signed-off-by: Kim Pepper --------- Signed-off-by: Kim Pepper --- src/OpenSearch/Client.php | 126 +----------------- .../Exceptions/Forbidden403Exception.php | 8 +- .../Common/Exceptions/Missing404Exception.php | 11 +- .../Exceptions/NoDocumentsToGetException.php | 11 +- .../Exceptions/NoNodesAvailableException.php | 8 ++ .../Exceptions/NoShardAvailableException.php | 12 +- .../Common/Exceptions/OpenSearchException.php | 12 +- .../Exceptions/RoutingMissingException.php | 12 +- .../Common/Exceptions/RuntimeException.php | 4 +- .../ScriptLangNotSupportedException.php | 12 +- .../ServerErrorResponseException.php | 5 + .../Exceptions/Unauthorized401Exception.php | 14 +- .../Exceptions/UnexpectedValueException.php | 4 +- src/OpenSearch/Connections/Connection.php | 19 ++- .../Exception/BadRequestHttpException.php | 17 +++ .../Exception/ConflictHttpException.php | 16 +++ .../Exception/ErrorMessageExtractor.php | 43 ++++++ .../Exception/ForbiddenHttpException.php | 16 +++ src/OpenSearch/Exception/HttpException.php | 34 +++++ .../Exception/HttpExceptionFactory.php | 71 ++++++++++ .../Exception/HttpExceptionInterface.php | 21 +++ .../InternalServerErrorHttpException.php | 16 +++ .../Exception/NoDocumentsToGetException.php | 12 ++ .../Exception/NoShardAvailableException.php | 12 ++ .../Exception/NotAcceptableHttpException.php | 11 ++ .../Exception/NotFoundHttpException.php | 16 +++ .../OpenSearchExceptionInterface.php | 12 ++ .../Exception/RequestTimeoutHttpException.php | 16 +++ .../Exception/RoutingMissingException.php | 12 ++ .../ScriptLangNotSupportedException.php | 12 ++ .../ServiceUnavailableHttpException.php | 16 +++ .../TooManyRequestsHttpException.php | 15 +++ .../Exception/UnauthorizedHttpException.php | 17 +++ src/OpenSearch/HttpTransport.php | 13 +- .../Namespaces/BooleanRequestWrapper.php | 12 +- src/OpenSearch/TransportInterface.php | 1 + tests/ClientIntegrationTest.php | 10 +- .../StaticConnectionPoolIntegrationTest.php | 1 + tests/Exception/ErrorMessageExtractorTest.php | 81 +++++++++++ tests/Exception/HttpExceptionFactoryTest.php | 67 ++++++++++ util/template/client-class | 126 +----------------- 41 files changed, 671 insertions(+), 283 deletions(-) create mode 100644 src/OpenSearch/Exception/BadRequestHttpException.php create mode 100644 src/OpenSearch/Exception/ConflictHttpException.php create mode 100644 src/OpenSearch/Exception/ErrorMessageExtractor.php create mode 100644 src/OpenSearch/Exception/ForbiddenHttpException.php create mode 100644 src/OpenSearch/Exception/HttpException.php create mode 100644 src/OpenSearch/Exception/HttpExceptionFactory.php create mode 100644 src/OpenSearch/Exception/HttpExceptionInterface.php create mode 100644 src/OpenSearch/Exception/InternalServerErrorHttpException.php create mode 100644 src/OpenSearch/Exception/NoDocumentsToGetException.php create mode 100644 src/OpenSearch/Exception/NoShardAvailableException.php create mode 100644 src/OpenSearch/Exception/NotAcceptableHttpException.php create mode 100644 src/OpenSearch/Exception/NotFoundHttpException.php create mode 100644 src/OpenSearch/Exception/OpenSearchExceptionInterface.php create mode 100644 src/OpenSearch/Exception/RequestTimeoutHttpException.php create mode 100644 src/OpenSearch/Exception/RoutingMissingException.php create mode 100644 src/OpenSearch/Exception/ScriptLangNotSupportedException.php create mode 100644 src/OpenSearch/Exception/ServiceUnavailableHttpException.php create mode 100644 src/OpenSearch/Exception/TooManyRequestsHttpException.php create mode 100644 src/OpenSearch/Exception/UnauthorizedHttpException.php create mode 100644 tests/Exception/ErrorMessageExtractorTest.php create mode 100644 tests/Exception/HttpExceptionFactoryTest.php diff --git a/src/OpenSearch/Client.php b/src/OpenSearch/Client.php index 45c1fc699..e97d6a28a 100644 --- a/src/OpenSearch/Client.php +++ b/src/OpenSearch/Client.php @@ -21,17 +21,6 @@ namespace OpenSearch; -use OpenSearch\Common\Exceptions\BadRequest400Exception; -use OpenSearch\Common\Exceptions\Conflict409Exception; -use OpenSearch\Common\Exceptions\Forbidden403Exception; -use OpenSearch\Common\Exceptions\Missing404Exception; -use OpenSearch\Common\Exceptions\NoDocumentsToGetException; -use OpenSearch\Common\Exceptions\NoShardAvailableException; -use OpenSearch\Common\Exceptions\RequestTimeout408Exception; -use OpenSearch\Common\Exceptions\RoutingMissingException; -use OpenSearch\Common\Exceptions\ScriptLangNotSupportedException; -use OpenSearch\Common\Exceptions\ServerErrorResponseException; -use OpenSearch\Common\Exceptions\Unauthorized401Exception; use OpenSearch\Endpoints\AbstractEndpoint; use OpenSearch\Namespaces\BooleanRequestWrapper; use OpenSearch\Namespaces\NamespaceBuilderInterface; @@ -106,11 +95,6 @@ class Client */ protected $registeredNamespaces = []; - /** - * @deprecated in 2.3.2 and will be removed in 3.0.0. - */ - private bool $throwExceptions = false; - /** * @var AsyncSearchNamespace */ @@ -284,13 +268,11 @@ class Client * @param TransportInterface|Transport $transport * @param callable|EndpointFactoryInterface $endpointFactory * @param NamespaceBuilderInterface[] $registeredNamespaces - * @param bool $throwExceptions */ public function __construct( TransportInterface|Transport $transport, callable|EndpointFactoryInterface $endpointFactory, array $registeredNamespaces, - bool $throwExceptions = false, ) { if (!$transport instanceof TransportInterface) { @trigger_error('Passing an instance of \OpenSearch\Transport to ' . __METHOD__ . '() is deprecated in 2.3.2 and will be removed in 3.0.0. Pass an instance of \OpenSearch\TransportInterface instead.', E_USER_DEPRECATED); @@ -311,13 +293,6 @@ public function __construct( } $this->endpoints = $endpoints; $this->endpointFactory = $endpointFactory; - if ($throwExceptions === true) { - @trigger_error( - 'The $throwExceptions parameter is deprecated in 2.4.0 and will be removed in 3.0.0. Check the response \'status_code\' instead', - E_USER_DEPRECATED - ); - $this->throwExceptions = true; - } $this->asyncSearch = new AsyncSearchNamespace($transport, $this->endpointFactory); $this->asynchronousSearch = new AsynchronousSearchNamespace($transport, $this->endpointFactory); $this->cat = new CatNamespace($transport, $this->endpointFactory); @@ -2098,7 +2073,7 @@ public function extractArgument(array &$params, string $arg) * Send a raw request to the cluster. * * @throws \Psr\Http\Client\ClientExceptionInterface - * @throws \OpenSearch\Common\Exceptions\OpenSearchException + * @throws \OpenSearch\Exception\HttpExceptionInterface */ public function request( string $method, @@ -2109,117 +2084,24 @@ public function request( $body = $attributes['body'] ?? null; $options = $attributes['options'] ?? []; - $response = $this->httpTransport->sendRequest($method, $uri, $params, $body, $options['headers'] ?? []); - - // @todo: Remove this in the next major release. - // Throw legacy exceptions. - if ($this->throwExceptions) { - if (isset($response['status']) && $response['status'] >= 400) { - $this->throwLegacyException($response); - } - } - - return $response; + return $this->httpTransport->sendRequest($method, $uri, $params, $body, $options['headers'] ?? []); } /** * Send a request for an endpoint. * * @throws \Psr\Http\Client\ClientExceptionInterface - * @throws \OpenSearch\Common\Exceptions\OpenSearchException + * @throws \OpenSearch\Exception\HttpExceptionInterface */ private function performRequest(AbstractEndpoint $endpoint): array|string|null { - $response = $this->httpTransport->sendRequest( + return $this->httpTransport->sendRequest( $endpoint->getMethod(), $endpoint->getURI(), $endpoint->getParams(), $endpoint->getBody(), $endpoint->getOptions() ); - - // @todo: Remove this in the next major release. - // Throw legacy exceptions. - if ($this->throwExceptions) { - if (isset($response['status']) && $response['status'] >= 400) { - $this->throwLegacyException($response); - } - } - - return $response; - } - - /** - * Throw legacy exceptions. - * - * @param array $response - * - * @throws \OpenSearch\Common\Exceptions\OpenSearchException - */ - private function throwLegacyException(array $response): void - { - if ($response['status'] >= 400 && $response['status'] < 500) { - $this->throwLegacyClientException($response); - } - if ($response['status'] >= 500) { - $this->throwLegacyServerException($response); - } - } - - /** - * Throw legacy client exceptions based on status code. - * - * @throws \OpenSearch\Common\Exceptions\OpenSearchException - */ - private function throwLegacyClientException($response): void - { - $statusCode = $response['status_code']; - $responseBody = $this->convertBodyToString($response['body'], $statusCode); - throw match ($statusCode) { - 401 => new Unauthorized401Exception($responseBody, $statusCode), - 403 => new Forbidden403Exception($responseBody, $statusCode), - 404 => new Missing404Exception($responseBody, $statusCode), - 409 => new Conflict409Exception($responseBody, $statusCode), - 400 => (str_contains($responseBody, 'script_lang not supported')) - ? new ScriptLangNotSupportedException($responseBody . $statusCode) - : new BadRequest400Exception($responseBody, $statusCode), - 408 => new RequestTimeout408Exception($responseBody, $statusCode), - default => new BadRequest400Exception($responseBody, $statusCode), - }; - } - - /** - * Throw legacy server exceptions based on status code. - * - * @throws \OpenSearch\Common\Exceptions\OpenSearchException - */ - private function throwLegacyServerException($response): void - { - $statusCode = $response['status_code']; - $error = $response['body']['error'] ?? []; - $reason = $error['reason'] ?? 'undefined reason'; - $type = $error['type'] ?? 'undefined type'; - $errorMessage = "$type: $reason"; - $responseBody = $this->convertBodyToString($response['body'], $statusCode); - - $exception = new ServerErrorResponseException($responseBody, $statusCode); - if ($statusCode === 500) { - if (str_contains($responseBody, "RoutingMissingException")) { - $exception = new RoutingMissingException($errorMessage, $statusCode); - } elseif (preg_match('/ActionRequestValidationException.+ no documents to get/', $responseBody) === 1) { - $exception = new NoDocumentsToGetException($errorMessage, $statusCode); - } elseif (str_contains($responseBody, 'NoShardAvailableActionException')) { - $exception = new NoShardAvailableException($errorMessage, $statusCode); - } - } - throw $exception; - } - - private function convertBodyToString(mixed $body, int $statusCode): string - { - return empty($body) - ? "Unknown $statusCode error from OpenSearch" - : (is_string($body) ? $body : json_encode($body)); } } diff --git a/src/OpenSearch/Common/Exceptions/Forbidden403Exception.php b/src/OpenSearch/Common/Exceptions/Forbidden403Exception.php index 8a307cd0c..62727f298 100644 --- a/src/OpenSearch/Common/Exceptions/Forbidden403Exception.php +++ b/src/OpenSearch/Common/Exceptions/Forbidden403Exception.php @@ -21,11 +21,15 @@ namespace OpenSearch\Common\Exceptions; -@trigger_error(Forbidden403Exception::class . ' is deprecated in 2.3.2 and will be removed in 3.0.0.', E_USER_DEPRECATED); +use OpenSearch\Exception\ForbiddenHttpException; + +@trigger_error(Forbidden403Exception::class . ' is deprecated in 2.3.2 and will be removed in 3.0.0. Use \OpenSearch\Exception\ForbiddenHttpException instead', E_USER_DEPRECATED); /** * @deprecated in 2.3.2 and will be removed in 3.0.0. + * + * @see \OpenSearch\Exception\ForbiddenHttpException */ -class Forbidden403Exception extends \Exception implements OpenSearchException +class Forbidden403Exception extends ForbiddenHttpException { } diff --git a/src/OpenSearch/Common/Exceptions/Missing404Exception.php b/src/OpenSearch/Common/Exceptions/Missing404Exception.php index c9b69be70..4db51a0ee 100644 --- a/src/OpenSearch/Common/Exceptions/Missing404Exception.php +++ b/src/OpenSearch/Common/Exceptions/Missing404Exception.php @@ -21,11 +21,18 @@ namespace OpenSearch\Common\Exceptions; -@trigger_error(Missing404Exception::class . ' is deprecated in 2.3.2 and will be removed in 3.0.0.', E_USER_DEPRECATED); +use OpenSearch\Exception\NotFoundHttpException; + +@trigger_error( + Missing404Exception::class . ' is deprecated in 2.3.2 and will be removed in 3.0.0. Use \OpenSearch\Exception\NotFoundHttpException instead.', + E_USER_DEPRECATED +); /** * @deprecated in 2.3.2 and will be removed in 3.0.0. + * + * @see \OpenSearch\Exception\NotFoundHttpException */ -class Missing404Exception extends \Exception implements OpenSearchException +class Missing404Exception extends NotFoundHttpException { } diff --git a/src/OpenSearch/Common/Exceptions/NoDocumentsToGetException.php b/src/OpenSearch/Common/Exceptions/NoDocumentsToGetException.php index 82fdd9fac..c08d8f6ee 100644 --- a/src/OpenSearch/Common/Exceptions/NoDocumentsToGetException.php +++ b/src/OpenSearch/Common/Exceptions/NoDocumentsToGetException.php @@ -21,11 +21,16 @@ namespace OpenSearch\Common\Exceptions; -@trigger_error(NoDocumentsToGetException::class . ' is deprecated in 2.3.2 and will be removed in 3.0.0.', E_USER_DEPRECATED); +@trigger_error( + NoDocumentsToGetException::class . ' is deprecated in 2.3.2 and will be removed in 3.0.0. Use \OpenSearch\Exception\NoDocumentsToGetException instead.', + E_USER_DEPRECATED +); /** - * @deprecated in 2.3.2 and will be removed in 3.0.0. + * @deprecated in 2.3.2 and will be removed in 3.0.0. Use \OpenSearch\Exception\NoDocumentsToGetException instead. + * + * @see \OpenSearch\Exception\ScriptLangNotSupportedException */ -class NoDocumentsToGetException extends ServerErrorResponseException implements OpenSearchException +class NoDocumentsToGetException extends \OpenSearch\Exception\NoDocumentsToGetException { } diff --git a/src/OpenSearch/Common/Exceptions/NoNodesAvailableException.php b/src/OpenSearch/Common/Exceptions/NoNodesAvailableException.php index dd7953064..00a4f06c8 100644 --- a/src/OpenSearch/Common/Exceptions/NoNodesAvailableException.php +++ b/src/OpenSearch/Common/Exceptions/NoNodesAvailableException.php @@ -21,6 +21,14 @@ namespace OpenSearch\Common\Exceptions; +@trigger_error( + NoNodesAvailableException::class . ' is deprecated in 2.3.2 and will be removed in 3.0.0.', + E_USER_DEPRECATED +); + +/** + * @deprecated in 2.3.2 and will be removed in 3.0.0. + */ class NoNodesAvailableException extends ServerErrorResponseException implements OpenSearchException { } diff --git a/src/OpenSearch/Common/Exceptions/NoShardAvailableException.php b/src/OpenSearch/Common/Exceptions/NoShardAvailableException.php index 88ee0fbb8..e79ba2a14 100644 --- a/src/OpenSearch/Common/Exceptions/NoShardAvailableException.php +++ b/src/OpenSearch/Common/Exceptions/NoShardAvailableException.php @@ -21,6 +21,16 @@ namespace OpenSearch\Common\Exceptions; -class NoShardAvailableException extends ServerErrorResponseException implements OpenSearchException +@trigger_error( + NoShardAvailableException::class . ' is deprecated in 2.3.2 and will be removed in 3.0.0. Use \OpenSearch\Exception\NoShardAvailableException instead.', + E_USER_DEPRECATED +); + +/** + * @deprecated in 2.3.2 and will be removed in 3.0.0. Use \OpenSearch\Exception\NoShardAvailableException instead. + * + * @see \OpenSearch\Exception\NoShardAvailableException + */ +class NoShardAvailableException extends \OpenSearch\Exception\NoShardAvailableException { } diff --git a/src/OpenSearch/Common/Exceptions/OpenSearchException.php b/src/OpenSearch/Common/Exceptions/OpenSearchException.php index 26eeaf917..116df579f 100644 --- a/src/OpenSearch/Common/Exceptions/OpenSearchException.php +++ b/src/OpenSearch/Common/Exceptions/OpenSearchException.php @@ -21,8 +21,16 @@ namespace OpenSearch\Common\Exceptions; -use Throwable; +use OpenSearch\Exception\OpenSearchExceptionInterface; -interface OpenSearchException extends Throwable +@trigger_error( + NoNodesAvailableException::class . ' is deprecated in 2.3.2 and will be removed in 3.0.0.', + E_USER_DEPRECATED +); + +/** + * @deprecated in 2.3.2 and will be removed in 3.0.0. + */ +interface OpenSearchException extends OpenSearchExceptionInterface { } diff --git a/src/OpenSearch/Common/Exceptions/RoutingMissingException.php b/src/OpenSearch/Common/Exceptions/RoutingMissingException.php index 97e97e4b0..cf5a4af16 100644 --- a/src/OpenSearch/Common/Exceptions/RoutingMissingException.php +++ b/src/OpenSearch/Common/Exceptions/RoutingMissingException.php @@ -21,6 +21,16 @@ namespace OpenSearch\Common\Exceptions; -class RoutingMissingException extends ServerErrorResponseException implements OpenSearchException +@trigger_error( + RoutingMissingException::class . ' is deprecated in 2.3.2 and will be removed in 3.0.0. Use \OpenSearch\Exception\RoutingMissingException instead.', + E_USER_DEPRECATED +); + +/** + * @deprecated in 2.3.2 and will be removed in 3.0.0. Use OpenSearch\Exception\UnauthorizedHttpException instead. + * + * @see \OpenSearch\Exception\ScriptLangNotSupportedException + */ +class RoutingMissingException extends \OpenSearch\Exception\RoutingMissingException { } diff --git a/src/OpenSearch/Common/Exceptions/RuntimeException.php b/src/OpenSearch/Common/Exceptions/RuntimeException.php index 1ad9adf33..09ab9af10 100644 --- a/src/OpenSearch/Common/Exceptions/RuntimeException.php +++ b/src/OpenSearch/Common/Exceptions/RuntimeException.php @@ -21,6 +21,8 @@ namespace OpenSearch\Common\Exceptions; -class RuntimeException extends \RuntimeException implements OpenSearchException +use OpenSearch\Exception\OpenSearchExceptionInterface; + +class RuntimeException extends \RuntimeException implements OpenSearchExceptionInterface { } diff --git a/src/OpenSearch/Common/Exceptions/ScriptLangNotSupportedException.php b/src/OpenSearch/Common/Exceptions/ScriptLangNotSupportedException.php index 35e611af9..8a3a3b9c5 100644 --- a/src/OpenSearch/Common/Exceptions/ScriptLangNotSupportedException.php +++ b/src/OpenSearch/Common/Exceptions/ScriptLangNotSupportedException.php @@ -21,6 +21,16 @@ namespace OpenSearch\Common\Exceptions; -class ScriptLangNotSupportedException extends BadRequest400Exception implements OpenSearchException +@trigger_error( + ScriptLangNotSupportedException::class . ' is deprecated in 2.3.2 and will be removed in 3.0.0. Use OpenSearch\Exception\ScriptLangNotSupportedException instead.', + E_USER_DEPRECATED +); + +/** + * @deprecated in 2.3.2 and will be removed in 3.0.0. Use OpenSearch\Exception\UnauthorizedHttpException instead. + * + * @see \OpenSearch\Exception\ScriptLangNotSupportedException + */ +class ScriptLangNotSupportedException extends \OpenSearch\Exception\ScriptLangNotSupportedException { } diff --git a/src/OpenSearch/Common/Exceptions/ServerErrorResponseException.php b/src/OpenSearch/Common/Exceptions/ServerErrorResponseException.php index 918466c41..84c273e61 100644 --- a/src/OpenSearch/Common/Exceptions/ServerErrorResponseException.php +++ b/src/OpenSearch/Common/Exceptions/ServerErrorResponseException.php @@ -21,6 +21,11 @@ namespace OpenSearch\Common\Exceptions; +@trigger_error(RequestTimeout408Exception::class . ' is deprecated in 2.3.2 and will be removed in 3.0.0.', E_USER_DEPRECATED); + +/** + * @deprecated in 2.3.2 and will be removed in 3.0.0. + */ class ServerErrorResponseException extends TransportException implements OpenSearchException { } diff --git a/src/OpenSearch/Common/Exceptions/Unauthorized401Exception.php b/src/OpenSearch/Common/Exceptions/Unauthorized401Exception.php index f477abe38..f3ddabdba 100644 --- a/src/OpenSearch/Common/Exceptions/Unauthorized401Exception.php +++ b/src/OpenSearch/Common/Exceptions/Unauthorized401Exception.php @@ -21,6 +21,18 @@ namespace OpenSearch\Common\Exceptions; -class Unauthorized401Exception extends \Exception implements OpenSearchException +use OpenSearch\Exception\UnauthorizedHttpException; + +@trigger_error( + Unauthorized401Exception::class . ' is deprecated in 2.3.2 and will be removed in 3.0.0. Use OpenSearch\Exception\UnauthorizedHttpException instead.', + E_USER_DEPRECATED +); + +/** + * @deprecated in 2.3.2 and will be removed in 3.0.0. Use OpenSearch\Exception\UnauthorizedHttpException instead. + * + * @see \OpenSearch\Exception\UnauthorizedHttpException + */ +class Unauthorized401Exception extends UnauthorizedHttpException { } diff --git a/src/OpenSearch/Common/Exceptions/UnexpectedValueException.php b/src/OpenSearch/Common/Exceptions/UnexpectedValueException.php index efd1e716d..657f1e86e 100644 --- a/src/OpenSearch/Common/Exceptions/UnexpectedValueException.php +++ b/src/OpenSearch/Common/Exceptions/UnexpectedValueException.php @@ -21,6 +21,8 @@ namespace OpenSearch\Common\Exceptions; -class UnexpectedValueException extends \UnexpectedValueException implements OpenSearchException +use OpenSearch\Exception\OpenSearchExceptionInterface; + +class UnexpectedValueException extends \UnexpectedValueException implements OpenSearchExceptionInterface { } diff --git a/src/OpenSearch/Connections/Connection.php b/src/OpenSearch/Connections/Connection.php index 653a1bcb0..8ca2a1bd4 100644 --- a/src/OpenSearch/Connections/Connection.php +++ b/src/OpenSearch/Connections/Connection.php @@ -664,19 +664,19 @@ private function process4xxError(array $request, array $response, array $ignore) $responseBody = $this->convertBodyToString($response['body'], $statusCode, $exception); if ($statusCode === 401) { - $exception = new Unauthorized401Exception($responseBody, $statusCode); + $exception = new Unauthorized401Exception($responseBody); } elseif ($statusCode === 403) { - $exception = new Forbidden403Exception($responseBody, $statusCode); + $exception = new Forbidden403Exception($responseBody); } elseif ($statusCode === 404) { - $exception = new Missing404Exception($responseBody, $statusCode); + $exception = new Missing404Exception($responseBody); } elseif ($statusCode === 409) { $exception = new Conflict409Exception($responseBody, $statusCode); } elseif ($statusCode === 400 && strpos($responseBody, 'script_lang not supported') !== false) { - $exception = new ScriptLangNotSupportedException($responseBody. $statusCode); + $exception = new ScriptLangNotSupportedException($responseBody); } elseif ($statusCode === 408) { - $exception = new RequestTimeout408Exception($responseBody, $statusCode); + $exception = new RequestTimeout408Exception($responseBody); } else { - $exception = new BadRequest400Exception($responseBody, $statusCode); + $exception = new BadRequest400Exception($responseBody); } $this->logRequestFail($request, $response, $exception); @@ -706,15 +706,14 @@ private function process5xxError(array $request, array $response, array $ignore) } if ($statusCode === 500 && strpos($responseBody, "RoutingMissingException") !== false) { - $exception = new RoutingMissingException($exception->getMessage(), $statusCode, $exception); + $exception = new RoutingMissingException($exception->getMessage(), [], 0, $exception); } elseif ($statusCode === 500 && preg_match('/ActionRequestValidationException.+ no documents to get/', $responseBody) === 1) { - $exception = new NoDocumentsToGetException($exception->getMessage(), $statusCode, $exception); + $exception = new NoDocumentsToGetException($exception->getMessage(), [], 0, $exception); } elseif ($statusCode === 500 && strpos($responseBody, 'NoShardAvailableActionException') !== false) { - $exception = new NoShardAvailableException($exception->getMessage(), $statusCode, $exception); + $exception = new NoShardAvailableException($exception->getMessage(), [], 0, $exception); } else { $exception = new ServerErrorResponseException( $this->convertBodyToString($responseBody, $statusCode, $exception), - $statusCode ); } diff --git a/src/OpenSearch/Exception/BadRequestHttpException.php b/src/OpenSearch/Exception/BadRequestHttpException.php new file mode 100644 index 000000000..8cc9bc7d1 --- /dev/null +++ b/src/OpenSearch/Exception/BadRequestHttpException.php @@ -0,0 +1,17 @@ +statusCode; + } + + public function getHeaders(): array + { + return $this->headers; + } + +} diff --git a/src/OpenSearch/Exception/HttpExceptionFactory.php b/src/OpenSearch/Exception/HttpExceptionFactory.php new file mode 100644 index 000000000..1d475345d --- /dev/null +++ b/src/OpenSearch/Exception/HttpExceptionFactory.php @@ -0,0 +1,71 @@ + self::createBadRequestException($errorMessage, $previous, $code, $headers), + 401 => new UnauthorizedHttpException($errorMessage, $headers, $code, $previous), + 403 => new ForbiddenHttpException($errorMessage, $headers, $code, $previous), + 404 => new NotFoundHttpException($errorMessage, $headers, $code, $previous), + 406 => new NotAcceptableHttpException($errorMessage, $headers, $code, $previous), + 409 => new ConflictHttpException($errorMessage, $headers, $code, $previous), + 429 => new TooManyRequestsHttpException($errorMessage, $headers, $code, $previous), + 500 => self::createInternalServerErrorException($errorMessage, $previous, $code, $headers), + 503 => new ServiceUnavailableHttpException($errorMessage, $headers, $code, $previous), + default => new HttpException($statusCode, $errorMessage, $headers, $code, $previous), + }; + } + + private static function createBadRequestException( + string $message = '', + ?\Throwable $previous = null, + int $code = 0, + array $headers = [] + ): HttpExceptionInterface { + if (str_contains($message, 'script_lang not supported')) { + return new ScriptLangNotSupportedException($message); + } + return new BadRequestHttpException($message, $headers, $code, $previous); + } + + /** + * Create an InternalServerErrorHttpException from the given parameters. + */ + private static function createInternalServerErrorException( + string $message = '', + ?\Throwable $previous = null, + int $code = 0, + array $headers = [] + ): HttpExceptionInterface { + if (str_contains($message, "RoutingMissingException")) { + return new RoutingMissingException($message); + } + if (preg_match('/ActionRequestValidationException.+ no documents to get/', $message) === 1) { + return new NoDocumentsToGetException($message); + } + if (str_contains($message, 'NoShardAvailableActionException')) { + return new NoShardAvailableException($message); + } + return new InternalServerErrorHttpException($message, $headers, $code, $previous); + } + +} diff --git a/src/OpenSearch/Exception/HttpExceptionInterface.php b/src/OpenSearch/Exception/HttpExceptionInterface.php new file mode 100644 index 000000000..2a3d76fb4 --- /dev/null +++ b/src/OpenSearch/Exception/HttpExceptionInterface.php @@ -0,0 +1,21 @@ +createRequest($method, $uri, $params, $body, $headers); $response = $this->client->sendRequest($request); - return $this->serializer->deserialize($response->getBody()->getContents(), $response->getHeaders()); + $statusCode = $response->getStatusCode(); + $responseBody = $response->getBody()->getContents(); + $responseHeaders = $response->getHeaders(); + $data = $this->serializer->deserialize($responseBody, $responseHeaders); + // Status code >= 200 < 300 is a success. + // Status code >= 300 < 400 is a redirect and should be handled by the client. + if ($statusCode >= 400) { + // Throw an HTTP exception. + throw HttpExceptionFactory::create($statusCode, $data); + } + return $data; } } diff --git a/src/OpenSearch/Namespaces/BooleanRequestWrapper.php b/src/OpenSearch/Namespaces/BooleanRequestWrapper.php index adf387a73..e5a45aa22 100644 --- a/src/OpenSearch/Namespaces/BooleanRequestWrapper.php +++ b/src/OpenSearch/Namespaces/BooleanRequestWrapper.php @@ -25,6 +25,7 @@ use OpenSearch\Common\Exceptions\Missing404Exception; use OpenSearch\Common\Exceptions\RoutingMissingException; use OpenSearch\Endpoints\AbstractEndpoint; +use OpenSearch\Exception\NotFoundHttpException; use OpenSearch\Transport; use OpenSearch\TransportInterface; @@ -41,7 +42,7 @@ abstract class BooleanRequestWrapper public static function sendRequest(AbstractEndpoint $endpoint, TransportInterface $transport): bool { try { - $response = $transport->sendRequest( + $transport->sendRequest( $endpoint->getMethod(), $endpoint->getURI(), $endpoint->getParams(), @@ -49,14 +50,11 @@ public static function sendRequest(AbstractEndpoint $endpoint, TransportInterfac $endpoint->getOptions() ); - return match ($response['status_code'] ?? null) { - 404 => false, - default => true, - }; - } catch (Missing404Exception|RoutingMissingException $e) { - // Handle legacy exceptions. + } catch (NotFoundHttpException|RoutingMissingException $e) { + // Return false for 404 errors. return false; } + return true; } /** diff --git a/src/OpenSearch/TransportInterface.php b/src/OpenSearch/TransportInterface.php index 5346fee0f..c2f432d30 100644 --- a/src/OpenSearch/TransportInterface.php +++ b/src/OpenSearch/TransportInterface.php @@ -15,6 +15,7 @@ interface TransportInterface * @param array $headers * * @throws \Psr\Http\Client\ClientExceptionInterface + * @throws \OpenSearch\Exception\HttpExceptionInterface */ public function sendRequest( string $method, diff --git a/tests/ClientIntegrationTest.php b/tests/ClientIntegrationTest.php index d6b807a8b..b2456c202 100644 --- a/tests/ClientIntegrationTest.php +++ b/tests/ClientIntegrationTest.php @@ -26,6 +26,7 @@ use OpenSearch\Client; use OpenSearch\Common\Exceptions\RuntimeException; use OpenSearch\EndpointFactory; +use OpenSearch\Exception\NotFoundHttpException; use OpenSearch\RequestFactory; use OpenSearch\Serializers\SmartSerializer; use OpenSearch\TransportFactory; @@ -63,15 +64,12 @@ public function testInfoNotEmpty() public function testNotFoundError() { - $result = $this->client->get([ + $this->expectException(NotFoundHttpException::class); + $this->expectExceptionMessage("index_not_found_exception: no such index [foo]"); + $this->client->get([ 'index' => 'foo', 'id' => 'bar', ]); - $this->assertEquals(404, $result['status']); - $error = $result['error']; - $this->assertEquals('index_not_found_exception', $error['type']); - $this->assertEquals('no such index [foo]', $error['reason']); - $this->assertEquals('foo', $error['index']); } public function testIndexCannotBeEmptyStringForDelete() diff --git a/tests/ConnectionPool/StaticConnectionPoolIntegrationTest.php b/tests/ConnectionPool/StaticConnectionPoolIntegrationTest.php index 0f3462c34..4bb3e058e 100644 --- a/tests/ConnectionPool/StaticConnectionPoolIntegrationTest.php +++ b/tests/ConnectionPool/StaticConnectionPoolIntegrationTest.php @@ -61,6 +61,7 @@ public function test404Liveness() $this->assertFalse($client->indices()->exists(['index' => 'not_existing_index'])); // But the node should be marked as alive since the server responded + $connection = $client->transport->getConnection(); $this->assertTrue($connection->isAlive()); } } diff --git a/tests/Exception/ErrorMessageExtractorTest.php b/tests/Exception/ErrorMessageExtractorTest.php new file mode 100644 index 000000000..2214a6fed --- /dev/null +++ b/tests/Exception/ErrorMessageExtractorTest.php @@ -0,0 +1,81 @@ + 'error message']; + $message = ErrorMessageExtractor::extractErrorMessage($data); + $this->assertEquals('{"foo":"error message"}', $message); + } + + public function testStringData(): void + { + $data = 'error message'; + $message = ErrorMessageExtractor::extractErrorMessage($data); + $this->assertEquals('error message', $message); + } + + public function testNullData(): void + { + $data = null; + $message = ErrorMessageExtractor::extractErrorMessage($data); + $this->assertNull($message); + } + + public function testLegacyErrorAsArray(): void + { + $data = [ + 'error' => [ + 'foo' => 'error message' + ] + ]; + $message = ErrorMessageExtractor::extractErrorMessage($data); + $this->assertEquals('{"foo":"error message"}', $message); + } + + public function testLegacyErrorAsString(): void + { + $data = [ + 'error' => 'error message' + ]; + $message = ErrorMessageExtractor::extractErrorMessage($data); + $this->assertEquals('error message', $message); + } + + public function testExtractMessageRootCause(): void + { + $data = [ + 'error' => [ + 'root_cause' => [ + [ + 'type' => 'root_cause_type', + 'reason' => 'root_cause_reason' + ] + ], + 'type' => 'type', + 'reason' => 'reason' + ], + 'status' => 400 + ]; + + $message = ErrorMessageExtractor::extractErrorMessage($data); + + $this->assertEquals('root_cause_type: root_cause_reason', $message); + } + + +} diff --git a/tests/Exception/HttpExceptionFactoryTest.php b/tests/Exception/HttpExceptionFactoryTest.php new file mode 100644 index 000000000..59dcc2be4 --- /dev/null +++ b/tests/Exception/HttpExceptionFactoryTest.php @@ -0,0 +1,67 @@ + 'bar']); + $this->assertInstanceOf(BadRequestHttpException::class, $exception); + $this->assertEquals('error message', $exception->getMessage()); + $this->assertEquals(['foo' => 'bar'], $exception->getHeaders()); + } + + public function testScriptLangNotSupportedException(): void + { + $exception = HttpExceptionFactory::create(400, 'script_lang not supported'); + $this->assertInstanceOf(ScriptLangNotSupportedException::class, $exception); + $this->assertEquals('script_lang not supported', $exception->getMessage()); + } + + public function testInternalServerError() + { + $exception = HttpExceptionFactory::create(500, 'error message', ['foo' => 'bar']); + $this->assertInstanceOf(InternalServerErrorHttpException::class, $exception); + $this->assertEquals('error message', $exception->getMessage()); + $this->assertEquals(['foo' => 'bar'], $exception->getHeaders()); + } + + public function testRoutingMissingException() + { + $exception = HttpExceptionFactory::create(500, 'RoutingMissingException'); + $this->assertInstanceOf(RoutingMissingException::class, $exception); + $this->assertEquals('RoutingMissingException', $exception->getMessage()); + } + + public function testNoDocumentsToGetException() + { + $exception = HttpExceptionFactory::create(500, 'ActionRequestValidationException foo bar no documents to get'); + $this->assertInstanceOf(NoDocumentsToGetException::class, $exception); + $this->assertEquals('ActionRequestValidationException foo bar no documents to get', $exception->getMessage()); + } + + public function testNoShardAvailableActionException() + { + $exception = HttpExceptionFactory::create(500, 'NoShardAvailableActionException foo bar'); + $this->assertInstanceOf(NoShardAvailableException::class, $exception); + $this->assertEquals('NoShardAvailableActionException foo bar', $exception->getMessage()); + } + +} diff --git a/util/template/client-class b/util/template/client-class index 073578a89..f0c28b54a 100644 --- a/util/template/client-class +++ b/util/template/client-class @@ -21,17 +21,6 @@ declare(strict_types=1); namespace OpenSearch; -use OpenSearch\Common\Exceptions\BadRequest400Exception; -use OpenSearch\Common\Exceptions\Conflict409Exception; -use OpenSearch\Common\Exceptions\Forbidden403Exception; -use OpenSearch\Common\Exceptions\Missing404Exception; -use OpenSearch\Common\Exceptions\NoDocumentsToGetException; -use OpenSearch\Common\Exceptions\NoShardAvailableException; -use OpenSearch\Common\Exceptions\RequestTimeout408Exception; -use OpenSearch\Common\Exceptions\RoutingMissingException; -use OpenSearch\Common\Exceptions\ScriptLangNotSupportedException; -use OpenSearch\Common\Exceptions\ServerErrorResponseException; -use OpenSearch\Common\Exceptions\Unauthorized401Exception; use OpenSearch\Endpoints\AbstractEndpoint; use OpenSearch\Namespaces\BooleanRequestWrapper; use OpenSearch\Namespaces\NamespaceBuilderInterface; @@ -75,11 +64,6 @@ class Client */ protected $registeredNamespaces = []; - /** - * @deprecated in 2.3.2 and will be removed in 3.0.0. - */ - private bool $throwExceptions = false; - :namespace_properties /** @@ -88,13 +72,11 @@ class Client * @param TransportInterface|Transport $transport * @param callable|EndpointFactoryInterface $endpointFactory * @param NamespaceBuilderInterface[] $registeredNamespaces - * @param bool $throwExceptions */ public function __construct( TransportInterface|Transport $transport, callable|EndpointFactoryInterface $endpointFactory, array $registeredNamespaces, - bool $throwExceptions = false, ) { if (!$transport instanceof TransportInterface) { @trigger_error('Passing an instance of \OpenSearch\Transport to ' . __METHOD__ . '() is deprecated in 2.3.2 and will be removed in 3.0.0. Pass an instance of \OpenSearch\TransportInterface instead.', E_USER_DEPRECATED); @@ -115,13 +97,6 @@ class Client } $this->endpoints = $endpoints; $this->endpointFactory = $endpointFactory; - if ($throwExceptions === true) { - @trigger_error( - 'The $throwExceptions parameter is deprecated in 2.4.0 and will be removed in 3.0.0. Check the response \'status_code\' instead', - E_USER_DEPRECATED - ); - $this->throwExceptions = true; - } :new-namespaces $this->registeredNamespaces = $registeredNamespaces; } @@ -172,7 +147,7 @@ class Client * Send a raw request to the cluster. * * @throws \Psr\Http\Client\ClientExceptionInterface - * @throws \OpenSearch\Common\Exceptions\OpenSearchException + * @throws \OpenSearch\Exception\HttpExceptionInterface */ public function request( string $method, @@ -183,117 +158,24 @@ class Client $body = $attributes['body'] ?? null; $options = $attributes['options'] ?? []; - $response = $this->httpTransport->sendRequest($method, $uri, $params, $body, $options['headers'] ?? []); - - // @todo: Remove this in the next major release. - // Throw legacy exceptions. - if ($this->throwExceptions) { - if (isset($response['status']) && $response['status'] >= 400) { - $this->throwLegacyException($response); - } - } - - return $response; + return $this->httpTransport->sendRequest($method, $uri, $params, $body, $options['headers'] ?? []); } /** * Send a request for an endpoint. * * @throws \Psr\Http\Client\ClientExceptionInterface - * @throws \OpenSearch\Common\Exceptions\OpenSearchException + * @throws \OpenSearch\Exception\HttpExceptionInterface */ private function performRequest(AbstractEndpoint $endpoint): array|string|null { - $response = $this->httpTransport->sendRequest( + return $this->httpTransport->sendRequest( $endpoint->getMethod(), $endpoint->getURI(), $endpoint->getParams(), $endpoint->getBody(), $endpoint->getOptions() ); - - // @todo: Remove this in the next major release. - // Throw legacy exceptions. - if ($this->throwExceptions) { - if (isset($response['status']) && $response['status'] >= 400) { - $this->throwLegacyException($response); - } - } - - return $response; - } - - /** - * Throw legacy exceptions. - * - * @param array $response - * - * @throws \OpenSearch\Common\Exceptions\OpenSearchException - */ - private function throwLegacyException(array $response): void - { - if ($response['status'] >= 400 && $response['status'] < 500) { - $this->throwLegacyClientException($response); - } - if ($response['status'] >= 500) { - $this->throwLegacyServerException($response); - } - } - - /** - * Throw legacy client exceptions based on status code. - * - * @throws \OpenSearch\Common\Exceptions\OpenSearchException - */ - private function throwLegacyClientException($response): void - { - $statusCode = $response['status_code']; - $responseBody = $this->convertBodyToString($response['body'], $statusCode); - throw match ($statusCode) { - 401 => new Unauthorized401Exception($responseBody, $statusCode), - 403 => new Forbidden403Exception($responseBody, $statusCode), - 404 => new Missing404Exception($responseBody, $statusCode), - 409 => new Conflict409Exception($responseBody, $statusCode), - 400 => (str_contains($responseBody, 'script_lang not supported')) - ? new ScriptLangNotSupportedException($responseBody . $statusCode) - : new BadRequest400Exception($responseBody, $statusCode), - 408 => new RequestTimeout408Exception($responseBody, $statusCode), - default => new BadRequest400Exception($responseBody, $statusCode), - }; - } - - /** - * Throw legacy server exceptions based on status code. - * - * @throws \OpenSearch\Common\Exceptions\OpenSearchException - */ - private function throwLegacyServerException($response): void - { - $statusCode = $response['status_code']; - $error = $response['body']['error'] ?? []; - $reason = $error['reason'] ?? 'undefined reason'; - $type = $error['type'] ?? 'undefined type'; - $errorMessage = "$type: $reason"; - $responseBody = $this->convertBodyToString($response['body'], $statusCode); - - $exception = new ServerErrorResponseException($responseBody, $statusCode); - if ($statusCode === 500) { - if (str_contains($responseBody, "RoutingMissingException")) { - $exception = new RoutingMissingException($errorMessage, $statusCode); - } elseif (preg_match('/ActionRequestValidationException.+ no documents to get/', $responseBody) === 1) { - $exception = new NoDocumentsToGetException($errorMessage, $statusCode); - } elseif (str_contains($responseBody, 'NoShardAvailableActionException')) { - $exception = new NoShardAvailableException($errorMessage, $statusCode); - } - } - throw $exception; - } - - private function convertBodyToString(mixed $body, int $statusCode): string - { - return empty($body) - ? "Unknown $statusCode error from OpenSearch" - : (is_string($body) ? $body : json_encode($body)); } }