Make WordPress Core

Changeset 63346


Ignore:
Timestamp:
08/25/2026 03:47:38 PM (9 hours ago)
Author:
gziolo
Message:

Catch Throwable instances in WP_AI_Client_Prompt_Builder

Updates the prompt builder constructor and proxied method calls to catch all Throwable instances. This ensures errors such as TypeError are converted to WP_Error objects instead of causing fatal errors.

Includes regression tests for throwable conversion and fluent method forwarding behavior.

Props khokansardar, gziolo, alaminfirdows, shanemuir, vedantere, r1k0, waneezashafiq32, ugyensupport, dhavalkapadane.
Fixes #65505.

Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php

    r63299 r63346  
    188188                try {
    189189                        $this->builder = new PromptBuilder( $registry, $prompt, AiClient::getEventDispatcher() );
    190                 } catch ( Exception $e ) {
     190                } catch ( Throwable $e ) {
    191191                        $this->builder = new PromptBuilder( $registry, null, AiClient::getEventDispatcher() );
    192                         $this->error   = $this->exception_to_wp_error( $e );
     192                        $this->error   = $this->throwable_to_wp_error( $e );
    193193                }
    194194
     
    378378
    379379                        return $result;
    380                 } catch ( Exception $e ) {
    381                         $this->error = $this->exception_to_wp_error( $e );
     380                } catch ( Throwable $e ) {
     381                        $this->error = $this->throwable_to_wp_error( $e );
    382382
    383383                        if ( self::is_generating_method( $name ) ) {
     
    392392
    393393        /**
    394          * Converts an exception into a WP_Error with a structured error code and message.
    395          *
    396          * This method maps different exception types to specific WP_Error codes and HTTP status codes.
     394         * Converts a throwable into a WP_Error with a structured error code and message.
     395         *
     396         * This method maps different throwable types to specific WP_Error codes and HTTP status codes.
    397397         * The presence of the status codes means these WP_Error objects can be easily used in REST API responses
    398398         * or other contexts where HTTP semantics are relevant.
     
    400400         * @since 7.0.0
    401401         *
    402          * @param Exception $e The exception to convert.
     402         * @param Throwable $throwable The throwable to convert.
    403403         * @return WP_Error The resulting WP_Error object.
    404404         */
    405         private function exception_to_wp_error( Exception $e ): WP_Error {
    406                 if ( $e instanceof NetworkException ) {
     405        private function throwable_to_wp_error( Throwable $throwable ): WP_Error {
     406                if ( $throwable instanceof NetworkException ) {
    407407                        $error_code  = 'prompt_network_error';
    408408                        $status_code = 503;
    409                 } elseif ( $e instanceof ClientException ) {
     409                } elseif ( $throwable instanceof ClientException ) {
    410410                        // `ClientException` uses HTTP status codes as exception codes, so we can rely on them.
    411411                        $error_code  = 'prompt_client_error';
    412                         $status_code = $e->getCode() ? $e->getCode() : 400;
    413                 } elseif ( $e instanceof ServerException ) {
     412                        $status_code = $throwable->getCode() ? $throwable->getCode() : 400;
     413                } elseif ( $throwable instanceof ServerException ) {
    414414                        // `ServerException` uses HTTP status codes as exception codes, so we can rely on them.
    415415                        $error_code  = 'prompt_upstream_server_error';
    416                         $status_code = $e->getCode() ? $e->getCode() : 500;
    417                 } elseif ( $e instanceof TokenLimitReachedException ) {
     416                        $status_code = $throwable->getCode() ? $throwable->getCode() : 500;
     417                } elseif ( $throwable instanceof TokenLimitReachedException ) {
    418418                        $error_code  = 'prompt_token_limit_reached';
    419419                        $status_code = 400;
    420                 } elseif ( $e instanceof InvalidArgumentException ) {
     420                } elseif ( $throwable instanceof InvalidArgumentException ) {
    421421                        $error_code  = 'prompt_invalid_argument';
    422422                        $status_code = 400;
     
    428428                return new WP_Error(
    429429                        $error_code,
    430                         $e->getMessage(),
     430                        $throwable->getMessage(),
    431431                        array(
    432432                                'status'          => $status_code,
    433                                 'exception_class' => get_class( $e ),
     433                                'exception_class' => get_class( $throwable ),
    434434                        )
    435435                );
  • trunk/tests/phpunit/tests/ai-client/wpAiClientPromptBuilder.php

    r63299 r63346  
    29122912
    29132913        /**
    2914          * Invokes the private exception_to_wp_error method via reflection.
     2914         * Invokes the private throwable_to_wp_error method via reflection.
    29152915         *
    29162916         * @param WP_AI_Client_Prompt_Builder $builder   The builder instance.
    2917          * @param Exception                   $exception The exception to convert.
     2917         * @param Throwable                   $throwable The throwable to convert.
    29182918         * @return WP_Error The resulting WP_Error.
    29192919         */
    2920         private function invoke_exception_to_wp_error( WP_AI_Client_Prompt_Builder $builder, Exception $exception ): WP_Error {
     2920        private function invoke_throwable_to_wp_error( WP_AI_Client_Prompt_Builder $builder, Throwable $throwable ): WP_Error {
    29212921                $reflection = new ReflectionClass( WP_AI_Client_Prompt_Builder::class );
    2922                 $method     = $reflection->getMethod( 'exception_to_wp_error' );
     2922                $method     = $reflection->getMethod( 'throwable_to_wp_error' );
    29232923                self::set_accessible( $method );
    29242924
    2925                 return $method->invoke( $builder, $exception );
     2925                return $method->invoke( $builder, $throwable );
    29262926        }
    29272927
     
    29332933        public function test_exception_to_wp_error_network_exception() {
    29342934                $builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry() );
    2935                 $error   = $this->invoke_exception_to_wp_error(
     2935                $error   = $this->invoke_throwable_to_wp_error(
    29362936                        $builder,
    29372937                        new NetworkException( 'Connection timed out' )
     
    29512951        public function test_exception_to_wp_error_client_exception_with_code() {
    29522952                $builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry() );
    2953                 $error   = $this->invoke_exception_to_wp_error(
     2953                $error   = $this->invoke_throwable_to_wp_error(
    29542954                        $builder,
    29552955                        new ClientException( 'Unauthorized', 401 )
     
    29692969        public function test_exception_to_wp_error_client_exception_without_code() {
    29702970                $builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry() );
    2971                 $error   = $this->invoke_exception_to_wp_error(
     2971                $error   = $this->invoke_throwable_to_wp_error(
    29722972                        $builder,
    29732973                        new ClientException( 'Bad request' )
     
    29862986        public function test_exception_to_wp_error_server_exception_with_code() {
    29872987                $builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry() );
    2988                 $error   = $this->invoke_exception_to_wp_error(
     2988                $error   = $this->invoke_throwable_to_wp_error(
    29892989                        $builder,
    29902990                        new ServerException( 'Bad gateway', 502 )
     
    30043004        public function test_exception_to_wp_error_server_exception_without_code() {
    30053005                $builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry() );
    3006                 $error   = $this->invoke_exception_to_wp_error(
     3006                $error   = $this->invoke_throwable_to_wp_error(
    30073007                        $builder,
    30083008                        new ServerException( 'Internal server error' )
     
    30213021        public function test_exception_to_wp_error_token_limit_reached_exception() {
    30223022                $builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry() );
    3023                 $error   = $this->invoke_exception_to_wp_error(
     3023                $error   = $this->invoke_throwable_to_wp_error(
    30243024                        $builder,
    30253025                        new TokenLimitReachedException( 'Token limit exceeded', 4096 )
     
    30393039        public function test_exception_to_wp_error_invalid_argument_exception() {
    30403040                $builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry() );
    3041                 $error   = $this->invoke_exception_to_wp_error(
     3041                $error   = $this->invoke_throwable_to_wp_error(
    30423042                        $builder,
    30433043                        new AiClientInvalidArgumentException( 'Invalid model parameter' )
     
    30573057        public function test_exception_to_wp_error_generic_exception() {
    30583058                $builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry() );
    3059                 $error   = $this->invoke_exception_to_wp_error(
     3059                $error   = $this->invoke_throwable_to_wp_error(
    30603060                        $builder,
    30613061                        new Exception( 'Something went wrong' )
     
    30693069
    30703070        /**
     3071         * Tests exception_to_wp_error maps an Error (e.g. TypeError) to a generic builder error.
     3072         *
     3073         * A TypeError extends Error, not Exception, so this guards against the
     3074         * conversion only accepting Exception instances.
     3075         *
     3076         * @ticket 65505
     3077         */
     3078        public function test_exception_to_wp_error_type_error() {
     3079                $builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry() );
     3080                $error   = $this->invoke_throwable_to_wp_error(
     3081                        $builder,
     3082                        new TypeError( 'Argument must be of type float' )
     3083                );
     3084
     3085                $this->assertSame( 'prompt_builder_error', $error->get_error_code() );
     3086                $this->assertSame( 'Argument must be of type float', $error->get_error_message() );
     3087                $this->assertSame( 500, $error->get_error_data()['status'] );
     3088                $this->assertSame( 'TypeError', $error->get_error_data()['exception_class'] );
     3089        }
     3090
     3091        /**
     3092         * Tests that a TypeError thrown by the wrapped SDK is caught and returned as a WP_Error.
     3093         *
     3094         * Passing an argument of the wrong type to a strict-typed SDK method throws a
     3095         * TypeError (which extends Error, not Exception). The builder must catch it and
     3096         * place itself in an error state instead of letting it fatal the request.
     3097         *
     3098         * @ticket 65505
     3099         */
     3100        public function test_call_catches_type_error_from_invalid_argument_type() {
     3101                $builder = new WP_AI_Client_Prompt_Builder( $this->registry, 'Test prompt' );
     3102
     3103                // usingTemperature() expects a float; an array can never be coerced and throws a TypeError.
     3104                $result = $builder->using_temperature( array( 0.7 ) );
     3105
     3106                // The builder is returned (fluent interface preserved), now in an error state.
     3107                $this->assertInstanceOf( WP_AI_Client_Prompt_Builder::class, $result );
     3108
     3109                // A generating method now surfaces the stored WP_Error rather than fataling.
     3110                $error = $result->generate_text();
     3111                $this->assertWPError( $error );
     3112                $this->assertSame( 'prompt_builder_error', $error->get_error_code() );
     3113        }
     3114
     3115        /**
    30713116         * Tests exception_to_wp_error always includes status and exception_class in error data.
    30723117         *
     
    30793124        public function test_exception_to_wp_error_error_data_structure( Exception $exception ) {
    30803125                $builder = new WP_AI_Client_Prompt_Builder( AiClient::defaultRegistry() );
    3081                 $error   = $this->invoke_exception_to_wp_error( $builder, $exception );
     3126                $error   = $this->invoke_throwable_to_wp_error( $builder, $exception );
    30823127
    30833128                $data = $error->get_error_data();
Note: See TracChangeset for help on using the changeset viewer.