Repository: WordPress/performance
Pull request #1934: Fix TypeError in `perflab_aao_query_autoloaded_options()` by serializing non-scalar option values
Author: login: SandiyosDev
Labels: name: [Type] Bug
description: An existing feature is broken
name: [Plugin] Performance Lab
description: Issue relates to work in the Performance Lab Plugin only
Created: 2025-03-17T21:18:35Z | Merged: 2025-03-17T22:51:44Z

## Summary

While running PHP 8.2.5, and visiting `/wp-admin/site-health.php`, the following fatal error occurred in our instance:

`strlen(): Argument #1 ($string) must be of type string, array given
`

this was triggered in `perflab_aao_query_autoloaded_options()` whenever `wp_load_alloptions()` returned an array or object, and the code called `strlen()` on that non-string, and since some object caching solutions can return unserialized data, it caused the fatal error above in PHP 8+


Addresses #

this PR updates `perflab_aao_query_autoloaded_options()` to wrap `$option_value` with `maybe_serialize()` before measuring its length to make sure it’s always a valid string

## Relevant technical choices

if the value is already a string, the function does nothing, if it’s an array or object, it gets validly serialized

<!--
For maintainers only, please make sure:

- PR has a `[Type]` label.
- PR has a plugin-specific milestone, or the `no milestone` label if it does not apply to any specific plugin.
- PR has a changelog-friendly title, or the `skip changelog` label if it should not be mentioned in the plugin's changelog.
-->



Diff:
diff --git a/plugins/performance-lab/includes/site-health/audit-autoloaded-options/helper.php b/plugins/performance-lab/includes/site-health/audit-autoloaded-options/helper.php
index 242cc6254c..41fe98dc42 100644
--- a/plugins/performance-lab/includes/site-health/audit-autoloaded-options/helper.php
+++ b/plugins/performance-lab/includes/site-health/audit-autoloaded-options/helper.php
@@ -147,6 +147,9 @@ function perflab_aao_query_autoloaded_options(): array {
 	$large_options = array();
 
 	foreach ( $all_options as $option_name => $option_value ) {
+		if ( is_array( $option_value ) || is_object( $option_value ) ) {
+			$option_value = serialize( $option_value ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
+		}
 		if ( strlen( $option_value ) > $option_threshold ) {
 			$large_options[] = (object) array(
 				'option_name'         => $option_name,
diff --git a/plugins/performance-lab/tests/includes/site-health/audit-autoloaded-options/test-audit-autoloaded-options.php b/plugins/performance-lab/tests/includes/site-health/audit-autoloaded-options/test-audit-autoloaded-options.php
index 8c5a557a15..609edcf8ea 100644
--- a/plugins/performance-lab/tests/includes/site-health/audit-autoloaded-options/test-audit-autoloaded-options.php
+++ b/plugins/performance-lab/tests/includes/site-health/audit-autoloaded-options/test-audit-autoloaded-options.php
@@ -115,6 +115,92 @@ static function ( $option ) {
 		$this->assertSame( $autoloaded_options_size + $expected_size_increase, perflab_aao_autoloaded_options_size() );
 	}
 
+	/**
+	 * Data provider.
+	 *
+	 * @return array<string, mixed>
+	 */
+	public function data_provider_test_perflab_aao_query_autoloaded_options(): array {
+		return array(
+			'none_over_threshold'           => array(
+				'alloptions' => array(
+					'foo'  => 'bar',
+					'o100' => str_repeat( 'a', 100 ),
+				),
+				'threshold'  => null,
+				'expected'   => array(),
+			),
+			'one_over_threshold'            => array(
+				'alloptions' => array(
+					'foo'  => 'bar',
+					'o101' => str_repeat( 'a', 101 ),
+				),
+				'threshold'  => null,
+				'expected'   => array( 'o101' ),
+			),
+			'three_over_custom_threshold'   => array(
+				'alloptions' => array(
+					'foo'  => 'bar',
+					'o51'  => str_repeat( 'a', 51 ),
+					'o100' => str_repeat( 'a', 100 ),
+					'o101' => str_repeat( 'a', 101 ),
+					'int'  => 123,
+					'bool' => true,
+				),
+				'threshold'  => 50,
+				'expected'   => array( 'o51', 'o100', 'o101' ),
+			),
+			'two_non_scalar_over_threshold' => array(
+				'alloptions' => array(
+					'foo'    => 'bar',
+					'array'  => array(
+						'key' => str_repeat( 'a', 101 ),
+					),
+					'object' => (object) array(
+						'key' => str_repeat( 'a', 101 ),
+					),
+				),
+				'threshold'  => null,
+				'expected'   => array( 'array', 'object' ),
+			),
+		);
+	}
+
+	/**
+	 * Tests perflab_aao_query_autoloaded_options().
+	 *
+	 * @dataProvider data_provider_test_perflab_aao_query_autoloaded_options
+	 * @covers ::perflab_aao_query_autoloaded_options
+	 *
+	 * @param array<string, mixed> $alloptions All options.
+	 * @param int|null             $threshold  Custom threshold.
+	 * @param string[]             $expected   Expected option names to be over the threshold.
+	 */
+	public function test_perflab_aao_query_autoloaded_options( array $alloptions, ?int $threshold, array $expected ): void {
+		add_filter(
+			'pre_wp_load_alloptions',
+			static function () use ( $alloptions ): array {
+				return $alloptions;
+			}
+		);
+
+		if ( isset( $threshold ) ) {
+			add_filter(
+				'perflab_aao_autoloaded_options_table_threshold',
+				static function () use ( $threshold ): int {
+					return $threshold;
+				}
+			);
+		}
+
+		$query = perflab_aao_query_autoloaded_options();
+		$this->assertCount( count( $expected ), $query );
+		$this->assertSameSets( $expected, wp_list_pluck( $query, 'option_name' ) );
+		foreach ( wp_list_pluck( $query, 'option_value_length' ) as $option_value_length ) {
+			$this->assertIsInt( $option_value_length );
+		}
+	}
+
 	public function test_perflab_aao_autoloaded_options_disable_revert_functionality(): void {
 
 		$user_id = self::factory()->user->create( array( 'role' => 'administrator' ) );


Reviews:
user: login: westonruter
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-17T21:31:44Z
user: login: westonruter
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-17T21:35:05Z
user: login: westonruter
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-17T21:39:02Z

Review comments:
diff_hunk: @@ -147,10 +147,11 @@ function perflab_aao_query_autoloaded_options(): array {
 	$large_options = array();
 
 	foreach ( $all_options as $option_name => $option_value ) {
-		if ( strlen( $option_value ) > $option_threshold ) {
+		$serialized_value = maybe_serialize( $option_value );
path: plugins/performance-lab/includes/site-health/audit-autoloaded-options/helper.php
user: login: westonruter
body: Following the pattern of #1238, I think this should be:

```suggestion
		if ( is_array( $option_value ) || is_object( $option_value ) ) {
			$option_value = serialize( $option_value ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
		}
```
created_at: 2025-03-17T21:31:19Z
author_association: MEMBER
diff_hunk: @@ -147,10 +147,11 @@ function perflab_aao_query_autoloaded_options(): array {
 	$large_options = array();
 
 	foreach ( $all_options as $option_name => $option_value ) {
-		if ( strlen( $option_value ) > $option_threshold ) {
+		$serialized_value = maybe_serialize( $option_value );
+		if ( strlen( $serialized_value ) > $option_threshold ) {
 			$large_options[] = (object) array(
 				'option_name'         => $option_name,
-				'option_value_length' => strlen( $option_value ),
+				'option_value_length' => strlen( $serialized_value ),
path: plugins/performance-lab/includes/site-health/audit-autoloaded-options/helper.php
user: login: westonruter
body: Then this can be changed back:

```suggestion
				'option_value_length' => strlen( $option_value ),
```
created_at: 2025-03-17T21:31:36Z
author_association: MEMBER
diff_hunk: @@ -147,7 +147,10 @@ function perflab_aao_query_autoloaded_options(): array {
 	$large_options = array();
 
 	foreach ( $all_options as $option_name => $option_value ) {
-		if ( strlen( $option_value ) > $option_threshold ) {
+		if ( is_array( $option_value ) || is_object( $option_value ) ) {
+			$option_value = serialize( $option_value ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
+		}
+		if ( strlen( $serialized_value ) > $option_threshold ) {
path: plugins/performance-lab/includes/site-health/audit-autoloaded-options/helper.php
user: login: westonruter
body: Oops. I didn't update this variable:

```suggestion
		if ( strlen( $option_value ) > $option_threshold ) {
```
created_at: 2025-03-17T21:35:05Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-17T21:18:48Z
body: The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the `props-bot` label.

## Unlinked Accounts

The following contributors have not linked their GitHub and WordPress.org accounts: @SandiyosDev.

Contributors, please [read how to link your accounts](https://make.wordpress.org/core/2020/03/19/associating-github-accounts-with-wordpress-org-profiles/) to ensure your work is properly credited in WordPress releases.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

```
Unlinked contributors: SandiyosDev.

Co-authored-by: westonruter <westonruter@git.wordpress.org>
```

**To understand the WordPress project's expectations around crediting contributors, please [review the Contributor Attribution page in the Core Handbook](https://make.wordpress.org/core/handbook/best-practices/contributor-attribution-props/).**

author_association: NONE
user: login: codecov[bot]
created_at: 2025-03-17T21:27:03Z
body: ## [Codecov](https://app.codecov.io/gh/WordPress/performance/pull/1934?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=WordPress) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 71.21%. Comparing base [(`61fa5d2`)](https://app.codecov.io/gh/WordPress/performance/commit/61fa5d2d595ec284719cea055ef51c532d8d6048?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=WordPress) to head [(`fce7677`)](https://app.codecov.io/gh/WordPress/performance/commit/fce7677074e45044857f3c16f8c0e8409fd08172?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=WordPress).
> Report is 8 commits behind head on trunk.

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##            trunk    #1934   +/-   ##
=======================================
  Coverage   71.20%   71.21%           
=======================================
  Files          86       86           
  Lines        6998     7000    +2     
=======================================
+ Hits         4983     4985    +2     
  Misses       2015     2015           
```

| [Flag](https://app.codecov.io/gh/WordPress/performance/pull/1934/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=WordPress) | Coverage Δ | |
|---|---|---|
| [multisite](https://app.codecov.io/gh/WordPress/performance/pull/1934/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=WordPress) | `71.21% <100.00%> (+<0.01%)` | :arrow_up: |
| [single](https://app.codecov.io/gh/WordPress/performance/pull/1934/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=WordPress) | `40.71% <100.00%> (+0.01%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=WordPress#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/WordPress/performance/pull/1934?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=WordPress).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=WordPress).

<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- 📦 [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: SandiyosDev
created_at: 2025-03-17T21:29:56Z
body: I wish to remain as an unlinked contributor for the time being, I'm only here to fix an issue I encountered.
author_association: CONTRIBUTOR
user: login: westonruter
created_at: 2025-03-17T21:30:00Z
body: @SandiyosDev Are you using Docket Cache by any chance?

This came up before: #1237.

It was addressed in `perflab_aao_autoloaded_options_size()` via #1238, but apparently it wasn't also implemented in `perflab_aao_query_autoloaded_options()`.
author_association: MEMBER
user: login: SandiyosDev
created_at: 2025-03-17T21:31:48Z
body: > Docket Cache

No, I switched our instance to using [atec-cache-apcu](https://wordpress.org/plugins/atec-cache-apcu/) from [redis-cache](https://wordpress.org/plugins/redis-cache/) since we're running a single instance
author_association: CONTRIBUTOR
user: login: westonruter
created_at: 2025-03-17T21:38:42Z
body: > No, I switched our instance to using [atec-cache-apcu](https://wordpress.org/plugins/atec-cache-apcu/) from [redis-cache](https://wordpress.org/plugins/redis-cache/) since we're running a single instance

Thanks. Technically then it seems [atec-cache-apcu](https://wordpress.org/plugins/atec-cache-apcu/) is not correctly implemented because it is causing `wp_load_alloptions()` to return something other than an array of serialized string values, which WordPress does here normally. Docket Cache is doing the same thing. In any case, we should be hardened against this scenario since at least two object cache plugins seem to be doing this.
author_association: MEMBER
user: login: westonruter
created_at: 2025-03-17T21:41:13Z
body: One thing that would nice to add is a test case for this as was added in #1238.
author_association: MEMBER
user: login: SandiyosDev
created_at: 2025-03-17T21:54:54Z
body: > One thing that would nice to add is a test case for this as was added in #1238.

@westonruter I'd agree with adding a test case for this PR, but I'm working through the GitHub web interface and codeserver with this PHP codebase binded, and since Codeserver is running in a non-PHP context, I can't properly validate my tests.
Could you assist me in adding the test case?
author_association: CONTRIBUTOR
user: login: westonruter
created_at: 2025-03-17T21:56:31Z
body: Yeah, someone can add a test if not me.
author_association: MEMBER
user: login: SandiyosDev
created_at: 2025-03-18T02:23:57Z
body: > Yeah, someone can add a test if not me.

thank you very much [westonruter](https://github.com/westonruter), you can keep the credit
author_association: CONTRIBUTOR