Opened 3 weeks ago
Last modified 30 hours ago
#65505 reviewing defect (bug)
WP_AI_Client_Prompt_Builder catches Exception but not Error, so a TypeError fatals the request
| Reported by: | khokansardar | Owned by: | |
|---|---|---|---|
| Priority: | normal | Milestone: | 7.2 |
| Component: | AI | Version: | trunk |
| Severity: | normal | Keywords: | has-patch has-unit-tests has-test-info 2nd-opinion |
| Cc: | Focuses: | php-compatibility |
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 (15)
This ticket was mentioned in PR #12256 on WordPress/wordpress-develop by @khokansardar.
3 weeks ago
#1
#2
@
3 weeks 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.
#4
@
3 weeks 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:
3 weeks 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.
#6
@
3 weeks 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!
#7
@
8 days ago
Reproduction Report
Environment
- WordPress: 7.1-alpha-62161-src
- Subdirectory: No
- PHP: 8.3.23
- Server: Apache/2.4.62 (Unix) OpenSSL/3.5.1 PHP/8.3.23
- Database: mysqli (Server: 8.0.39 / Client: mysqlnd 8.3.23)
- Browser: Chrome 149.0.0.0
- OS: macOS
- Theme: Twenty Twenty-Five 1.5
- MU Plugins: None activated
- Plugins:
- Test Reports 1.3.0
Steps taken
- Confirmed the site was running WordPress 7.1-alpha-62161-src.
- Ran the following WP-CLI command:
wp eval ’ $result = wp_ai_client_prompt() ->with_text( array( “This should be a string” ) ) ->generate_text(); var_dump( $result ); ’
- Observed that passing an array to with_text(), which expects a string, caused an uncaught TypeError.
- Confirmed from the stack trace that the error passed through WP_AI_Client_Prompt_Builder->call() and terminated execution rather than being converted into a WP_Error.
🐞 Bug occurs
Expected behavior
- The TypeError should be caught by WP_AI_Client_Prompt_Builder and converted into a WP_Error.
- Execution should not terminate with an uncaught fatal error.
Additional Notes
- The issue was reproduced without configuring an AI provider or API credentials.
- The error occurs while building the prompt, before any request to an AI provider is made.
- The fatal error originates from WordPress\AiClient\Builders\PromptBuilder::withText() after an array is passed instead of the required string.
- The stack trace confirms the error passes through WP_AI_Client_Prompt_Builder->call().
The relevant fatal error was:
PHP Fatal error: Uncaught TypeError: WordPress\AiClient\Builders\PromptBuilder::withText(): Argument #1 ($text) must be of type string, array given, called in /Users/shanem/Desktop/projects/wordpress-trunk/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php on line 353
Support Content
- Reproduction command:
wp eval ’ $result = wp_ai_client_prompt() ->with_text( array( “This should be a string” ) ) ->generate_text(); var_dump( $result ); ’
- Result:
PHP Fatal error: Uncaught TypeError: WordPress\AiClient\Builders\PromptBuilder::withText(): Argument #1 ($text) must be of type string, array given
#8
@
8 days ago
Test Report
Patch tested: https://patch-diff.githubusercontent.com/raw/WordPress/wordpress-develop/pull/12256.diff
Environment
- WordPress: 7.1-alpha-62161-src
- Subdirectory: No
- PHP: 8.3.23
- Server: Apache/2.4.62 (Unix) OpenSSL/3.5.1 PHP/8.3.23
- Database: mysqli (Server: 8.0.39 / Client: mysqlnd 8.3.23)
- Browser: Chrome 149.0.0.0
- OS: macOS
- Theme: Twenty Twenty-Five 1.5
- MU Plugins: None activated
- Plugins:
- Test Reports 1.3.0
Steps taken
- Confirmed the issue was reproducible before applying the patch.
- Ran a WP-CLI command that passed an array to with_text(), which expects a string.
- Confirmed the unpatched code caused an uncaught TypeError and terminated execution with a fatal error.
- Applied the patch from PR 12256.
- Ran the same test again using the following WP-CLI command:
wp eval ’
$result = wp_ai_client_prompt()
->with_text( array( “This should be a string” ) )
->generate_text();
echo “Is WP_Error: “ . ( is_wp_error( $result ) ? “yes” : “no” ) . PHP_EOL;
if ( is_wp_error( $result ) ) {
echo “Code: “ . $result->get_error_code() . PHP_EOL;
echo “Message: “ . $result->get_error_message() . PHP_EOL;
print_r( $result->get_error_data() );
}
’
- Confirmed the TypeError was caught and converted into a WP_Error instead of causing a fatal error.
✅ Patch is solving the problem
Expected result
- The TypeError should be caught by WP_AI_Client_Prompt_Builder.
- Execution should not terminate with an uncaught fatal error.
- The error should be converted into a WP_Error.
- The returned WP_Error should include the original exception class and an appropriate HTTP status.
Additional Notes
- Before applying the patch, the same test caused an uncaught TypeError and terminated execution.
- After applying the patch, execution completed normally and returned a WP_Error.
- The returned error contained:
- Error code: prompt_builder_error
- Status: 500
- Exception class: TypeError
- No AI provider or API credentials were required to reproduce the issue or test the patch.
The result after applying the patch was:
Is WP_Error: yes Code: prompt_builder_error Message: WordPress\AiClient\Builders\PromptBuilder::withText(): Argument #1 ($text) must be of type string, array given, called in /Users/shanem/Desktop/projects/wordpress-trunk/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php on line 353 Array ( [status] => 500 [exception_class] => TypeError )
Support Content
- Test command:
wp eval ’
$result = wp_ai_client_prompt()
->with_text( array( “This should be a string” ) )
->generate_text();
echo “Is WP_Error: “ . ( is_wp_error( $result ) ? “yes” : “no” ) . PHP_EOL;
if ( is_wp_error( $result ) ) {
echo “Code: “ . $result->get_error_code() . PHP_EOL;
echo “Message: “ . $result->get_error_message() . PHP_EOL;
print_r( $result->get_error_data() );
}
’
- Before applying the patch:
- Uncaught TypeError
- Fatal error
- Execution terminated
- After applying the patch:
- WP_Error returned
- Error code: prompt_builder_error
- Exception class: TypeError
- Status: 500
#9
@
7 days ago
Test Report
Verified locally against trunk (docker-based dev env: PHP 8.5.6, MySQL 9.7).
Before patch
Reproduced both fatals:
wp_ai_client_prompt('Write a haiku')->using_temperature([0.7])->generate_text()wp_ai_client_prompt()->with_text([...])->generate_text()
Both throw an uncaught TypeError and crash with a fatal error, exactly as described in the ticket and in comment #7.
After applying the patch (gh pr diff 12256 applied cleanly on trunk, no conflicts)
Both cases now return a proper WP_Error instead of fataling:
`
Is WP_Error: yes
Code: prompt_builder_error
Message: WordPress\AiClient\Builders\PromptBuilder::usingTemperature(): Argument #1 ($temperature) must be of type float, array given, ...
Array
(
[status] => 500
[exception_class] => TypeError
)
`
Test suite
`
vendor/bin/phpunit -c phpunit.xml.dist --group ai-client
...
Time: 00:00.662, Memory: 213.00 MB
OK (254 tests, 647 assertions)
`
(254/647 vs. the 181/463 reported in the PR description — trunk appears to have gained more ai-client tests since the PR was opened; nothing regressed.)
Patch resolves the issue with no regressions in the ai-client suite.
@khokansardar commented on PR #12256:
7 days ago
#10
To make it more readable and consistent, I prefer using
$thinstead of$eor$throwable. We already use$thin many other places, so it also keeps the naming consistent.
reference:
Thanks for the reference. The Psr18Client file is part of the bundled SimplePie library (External), so it follows its own conventions rather than core's. In core itself, the established pattern when catching Throwable is $throwable — see template.php, where catch ( Throwable $throwable ) is used.
To stay consistent with core, I'll standardize on $throwable across both catch blocks and the throwable_to_wp_error() parameter. Let me know if you feel differently.
This ticket was mentioned in Slack in #core-test by nikunj8866. View the logs.
7 days ago
#13
@
6 days ago
Test Report
Patch tested: https://github.com/WordPress/wordpress-develop/pull/12256
Environment
- WordPress: 7.1-alpha-62161-src
- Subdirectory: No
- PHP: 8.3.31
- Server: nginx/1.31.1
- Database: mysqli (Server: 8.4.9 / Client: mysqlnd 8.3.31)
- Browser: Chrome 150.0.0.0
- OS: Linux
- Theme: Twenty Twenty-Five 1.5
- MU Plugins: None activated
- Plugins:
- Test Reports 1.3.0
Steps taken
- Ensure
wordpress-developis running, then run the command below in your terminal before applying the patch.npm run env:cli eval ' $text = wp_ai_client_prompt( "What version is WordPress currently on?" ) ->using_temperature( "this is not allowed" ) ->generate_text(); var_dump( $text );' - Observe the error message right in the terminal or inside
/wp-content/debug.log - Apply the patch and run the code below:
npm run env:cli eval ' $text = wp_ai_client_prompt( "What version is WordPress currently on?" ) ->using_temperature( "this is not allowed" ) ->generate_text(); if ( is_wp_error( $text ) ) { echo $text->get_error_code() . PHP_EOL; echo $text->get_error_message() . PHP_EOL; var_dump( $text->get_error_data() ); }' - Observe the output in the terminal.
- ✅ Patch is solving the problem
Expected result
- The builder enters an error state and returns a
WP_ERRORinstead of a fatal error
Additional Notes
- This also applies to other invalid argument types. Eg:
using_max_tokens( "lots" ) //using a string
with_text( array( 29 ) ) //using an array
- Updating keyword by removing
needs-testing, add it back if further testing is needed.
Results
- Fatal Error in debug log Before:
[10-Jul-2026 05:52:33 UTC] PHP Fatal error: Uncaught TypeError: WordPress\AiClient\Builders\PromptBuilder::usingTemperature(): Argument #1 ($temperature) must be of type float, string given, called in /var/www/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php on line 353 and defined in /var/www/src/wp-includes/php-ai-client/src/Builders/PromptBuilder.php:362
- WP_Error After:
prompt_builder_error WordPress\AiClient\Builders\PromptBuilder::usingTemperature(): Argument #1 ($temperature) must be of type float, string given, called in /var/www/src/wp-includes/ai-client/class-wp-ai-client-prompt-builder.php on line 353 array(2) { ["status"]=> int(500) ["exception_class"]=> string(9) "TypeError" }
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)
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 aWP_Errorfrom a generating method. The constructor and the__call()proxy enforce this with try/catch, but both only caughtException.__call()forwards caller arguments into the strict-typed php-ai-client SDK (usingTemperature(float),usingMaxTokens(int), etc. — the SDK usesstrict_types=1). A wrong argument type throws aTypeError, which extendsError, notException. The existing catch missed it, so it escaped uncaught and fataled the request (HTTP 500) instead of returning aWP_Error.### Steps to reproduce
Changes
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.