Skip to content

Customize: Keep stored custom CSS post ID when the lookup fails. - #14225

Open
wpexplorer wants to merge 1 commit into
WordPress:trunkfrom
wpexplorer:54443-custom-css-lookup
Open

wpexplorer wants to merge 1 commit into
WordPress:trunkfrom
wpexplorer:54443-custom-css-lookup

Conversation

@wpexplorer

Copy link
Copy Markdown

Keeps a stored custom_css_post_id when the custom CSS post lookup fails, instead of replacing it with -1. A failed lookup (e.g. during a database error) no longer hides an existing custom_css post or causes a duplicate to be created on the next save. Sites without custom CSS still cache -1.

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

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5.5
Used for: Investigating the issue on a production site and drafting the change; the final change was reviewed and edited by me.


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

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.

Core Committers: Use this line as a base for the props when committing in SVN:

Props wpexplorer, domainsupport.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@webd-uk

webd-uk commented Oct 13, 2026 •

Copy link
Copy Markdown

Great to see this being picked up.

The improvement to my original suggested fix in Trac ticket #54443 looks like exactly what's required.

Would it be worth adding unit tests to cover the three scenarios: an existing custom CSS post being found, a lookup failure with a previously stored post ID, and a site with no custom CSS post?

Looking forward to seeing this make it into WordPress Core!

@rejaulalomkhan rejaulalomkhan left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this, @wpexplorer, and for writing up the production case on the ticket! Keeping a stored ID is a small, focused change, and it leaves the -1 cache in place for sites that never saved custom CSS.

I checked it in the wordpress-develop Docker env (PHP 8.3, MySQL 9.7) by forcing every query on wp_posts to fail during wp_get_custom_css_post():

Scenario Trunk PR
Lookup fails with a stored ID custom_css_post_id reset to -1 Stored ID kept ✅
No custom CSS post, no stored ID -1 cached -1 cached ✅
Post deleted out of band (the trade-off from comment:9) Self-heals to -1, then 1 query per call ID stays stale, 3 queries per call until CSS is saved again

(The query counts are without a persistent object cache; I flushed the cache between calls.)

I left two notes inline: tests, and a question on the trade-off.

AI Review: Claude Opus 5.5

Comment thread src/wp-includes/theme.php
* @todo This should get cleared if a custom_css post is added/removed.
*/
set_theme_mod( 'custom_css_post_id', $post ? $post->ID : -1 );
if ( $post || empty( $post_id ) ) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should fix: This changes behaviour but has no test yet, as @webd-uk also asked. A DB failure is easy to simulate with the query filter. The test below fails on trunk (Failed asserting that -1 is identical to <ID>) and passes with this PR. I ran it locally, so it could go into tests/phpunit/tests/customize/custom-css-setting.php or a new tests/phpunit/tests/theme/wpGetCustomCssPost.php:

/**
 * @ticket 54443
 *
 * @covers ::wp_get_custom_css_post
 */
public function test_stored_post_id_is_kept_when_lookup_fails() {
	global $wpdb;

	$post = wp_update_custom_css_post( 'body { color: red; }' );

	$suppress = $wpdb->suppress_errors( true );
	$break    = static function ( $query ) use ( $wpdb ) {
		return str_contains( $query, $wpdb->posts ) ? 'SELECT broken FROM missing_table' : $query;
	};
	add_filter( 'query', $break );
	clean_post_cache( $post->ID );

	$this->assertNull( wp_get_custom_css_post(), 'The lookup should fail.' );

	remove_filter( 'query', $break );
	$wpdb->suppress_errors( $suppress );

	$this->assertSame( $post->ID, get_theme_mod( 'custom_css_post_id' ), 'The stored post ID should be kept.' );
}

A second test for a site with no custom CSS post caching -1 would cover the other branch.

Comment thread src/wp-includes/theme.php
*/
set_theme_mod( 'custom_css_post_id', $post ? $post->ID : -1 );
if ( $post || empty( $post_id ) ) {
set_theme_mod( 'custom_css_post_id', $post ? $post->ID : -1 );

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Would checking $wpdb->last_error right after the WP_Query work here instead? In the failure simulation above, last_error is set at that point. Skipping set_theme_mod() only when the query errored would protect the stored ID and still let a really deleted post fall back to -1, so there are no repeated lookups. I know @dlh found diagnosing DB errors here a bit out of scope in comment:3, so this is only an option for whoever commits it. The current approach is fine for the case in the ticket. Roughly (with global $wpdb; added to the function):

if ( ! $post && $wpdb->last_error ) {
	// The lookup failed; keep the stored ID rather than caching "no post".
} else {
	set_theme_mod( 'custom_css_post_id', $post ? $post->ID : -1 );
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to add some real-world context to this trade-off.

I've encountered orphaned custom CSS posts dozens of times over the years across both my own and clients' WordPress installations. I can't 100% say every instance was caused by this particular bug, but it's certainly a recurring problem.

On the other hand, I've never encountered anyone manually deleting a custom CSS post directly from the database. Doing so bypasses WordPress's normal housekeeping anyway, so some resulting inconsistencies are to be expected.

Personally, I'd favour keeping the fix simple rather than introducing additional complexity to accommodate an edge case that, in my experience at least, is considerably less likely than the problem we're trying to solve.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On the manually deleted case, I agree with @webd-uk. Deleting the custom_css post directly in the database skips WordPress's own housekeeping, so it's already "doing it wrong". Once the CSS is saved again it fixes itself, until then the cost is a few extra queries per request. I think that's a better trade-off than a site losing its CSS completely.

I originally thought about checking $wpdb->last_error first and decided against it for the following reasons:

  1. @dlh pointed out in comment:3 that diagnosing database errors in this function is out of scope, and his comment:6 points to the approach this PR takes instead.
  2. It doesn't catch every failure. If the connection drops and reconnecting fails after template_redirect (custom CSS loads on wp_head), wpdb::query() returns false without setting last_error. -1 would still be cached in that case.
  3. last_error only describes the most recent query. A plugin running its own query from posts_results or the_posts would overwrite it before the check.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants