Make WordPress Core

Opened 7 days ago

Last modified 14 hours ago

#65505 new defect (bug)

WP_AI_Client_Prompt_Builder catches Exception but not Error, so a TypeError fatals the request

Reported by: khokansardar's profile khokansardar Owned by:
Milestone: 7.1 Priority: normal
Severity: normal Version: trunk
Component: AI Keywords: has-patch has-unit-tests needs-testing has-test-info
Focuses: php-compatibility Cc:

Description

WP_AI_Client_Prompt_Builder documents that "as soon as any exception is caught in a chain of method calls, the returned instance will be in an error state … the WP_Error will be returned" (class docblock). To honor that, both the constructor and the magic __call() proxy wrap their work in try/catch.

However, both only catch ( Exception $e ):

  • src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php:188 (constructor)
  • src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php:361 (call)

__call() forwards the caller's arguments straight into the strict-typed php-ai-client SDK (usingTemperature(float), usingMaxTokens(int), withText(string), … — the SDK declares strict_types=1). When a caller passes an argument of an incompatible type, PHP throws a TypeError.

TypeError extends Error, not Exception, so the existing catch does not match.
The error escapes uncaught and produces a PHP fatal / HTTP 500, instead of being converted into the WP_Error the contract promises and the fluent error-state machinery never engages.

Steps to reproduce:

<?php
$builder = wp_ai_client_prompt( 'Write a haiku' );
// Array can never be coerced to float -> TypeError from usingTemperature().
$result = $builder->using_temperature( array( 0.7 ) )->generate_text();
// Expected: WP_Error. Actual: uncaught TypeError -> fatal.

Note the builder file itself runs in coercive typing mode, so numeric strings are still coerced; the fatal occurs for genuinely uncoercible types (arrays, objects, non-numeric strings such as using_max_tokens( 'lots' )).

Fix: catch Throwable instead of Exception in both locations, and widen exception_to_wp_error() to accept Throwable. Its existing instanceof ladder already falls through to the generic prompt_builder_error / 500 case, so any Error maps cleanly to a WP_Error with no further change.

Change History (6)

This ticket was mentioned in PR #12256 on WordPress/wordpress-develop by @khokansardar.


7 days ago
#1

WP_AI_Client_Prompt_Builder's contract is that any failure during a method chain puts the builder into an error state and is surfaced as a WP_Error from a generating method. The constructor and the __call() proxy enforce this with try/catch, but both only caught Exception.

__call() forwards caller arguments into the strict-typed php-ai-client SDK (usingTemperature(float), usingMaxTokens(int), etc. — the SDK uses strict_types=1). A wrong argument type throws a TypeError, which extends Error, not Exception. The existing catch missed it, so it escaped uncaught and fataled the request (HTTP 500) instead of returning a WP_Error.

### Steps to reproduce

$builder = wp_ai_client_prompt( 'Write a haiku' );
// An array can never be coerced to float -> TypeError from usingTemperature().
$result = $builder->using_temperature( array( 0.7 ) )->generate_text();
// Expected: WP_Error. Before this change: uncaught TypeError -> fatal.

Changes

  • Catch Throwable instead of Exception in the constructor and call().
  • Widen exception_to_wp_error() to accept Throwable. Its instanceof ladder already falls through to prompt_builder_error / 500 for unrecognized types, so Error/TypeError map cleanly with no other change.
  • Add regression tests:
    • exception_to_wp_error() maps a TypeError to prompt_builder_error / 500.
    • End-to-end: passing an uncoercible argument type to a wrapped SDK method is caught and surfaced as a WP_Error from a generating method instead of fataling.

Testing instructions

Run the AI Client suite:

npm run test:php -- --group ai-client

181 tests, 463 assertions, all passing.

Trac ticket: https://core.trac.wordpress.org/ticket/65505

## Use of AI Tools

AI assistance: Yes
Tool(s): Claude
Used for: Used for test cases. Reviewed and edited by me.

#2 @gziolo
4 days ago

@westonruter or @peterwilsoncc, how would this change play out under WordPress's strict backward compatibility policy? Everything is private, so we should be good to extend support to Throwable.

Last edited 4 days ago by gziolo (previous) (diff)

#3 @gziolo
4 days ago

  • Milestone changed from Awaiting Review to 7.1

#4 @khokansardar
4 days ago

hi @gziolo , could you please take a look at this issue and the associated PR when you have a chance? https://github.com/WordPress/wordpress-develop/pull/12256

@alaminfirdows commented on PR #12256:


15 hours ago
#5

To make it more readable and consistent, I prefer using $th instead of $e or $throwable. We already use $th in many other places, so it also keeps the naming consistent.

reference: https://github.com/WordPress/wordpress-develop/blob/2a5a37e54b1474f24e3f49a9345b22c49ff6e2e4/src/wp-includes/SimplePie/src/HTTP/Psr18Client.php#L152

#6 @alaminfirdows
14 hours ago

  • Keywords has-test-info added

Tested on macOS (Darwin 24.6.0) against the current wordpress-develop branch.

Environment

  • OS: macOS 15 (Darwin 24.6.0)
  • Local environment: WordPress Develop
  • WP-CLI: Available
  • Test method: wp eval-file

What I tested

Verified that the prompt builder correctly handles invalid argument types passed to using_temperature().

Result

✅ Confirmed that the TypeError is caught and converted into a WP_Error (prompt_builder_error) instead of causing an uncaught fatal error (HTTP 500).

The error handling behaves as expected and I did not observe any regressions during this verification.

Thanks for the fix!

Note: See TracTickets for help on using tickets.