Rewrite Rules: Treat a request path of "0" as a real request - #12740
Rewrite Rules: Treat a request path of "0" as a real request#12740MicahelE wants to merge 1 commit into
Conversation
A request path consisting of the string "0" is falsy in PHP, so several truthiness guards treated it as though no path had been requested at all. With a permalink structure of `/%category%/%postname%/`, a request for `/0/` skipped the rewrite rule loop entirely and silently fell through to the front page instead of resolving the `(.+?)/?$` catch-all rule and 404ing. Three checks are corrected: * `WP::parse_request()` only short-circuits to the `^$` rule when the requested path is actually an empty string, so "0" is matched against the rewrite rules. * `WP_Query::parse_tax_query()` builds a taxonomy clause for a term slug of "0", which previously was skipped by `empty()`. * `WP_Query::get_queried_object()` looks up a category whose slug is "0". Props micahele. Fixes #57858.
|
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 Core Committers: Use this line as a base for the props when committing in SVN: To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
Test using WordPress PlaygroundThe 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
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
There was a problem hiding this comment.
Pull request overview
This PR fixes a WordPress rewrite/query edge case where a requested path segment of "0" (falsy in PHP) was incorrectly treated as “no request”, causing /0/ to fall through to the front page instead of being processed by rewrite rules (and typically returning a 404). It also ensures taxonomy/category queries correctly handle a term slug of "0".
Changes:
- Adjusts
WP::parse_request()to treat""(empty string) as empty, rather than usingempty()which misclassifies"0". - Updates
WP_Query::parse_tax_query()andWP_Query::get_queried_object()to correctly handle taxonomy/category query vars whose value is"0". - Adds a focused PHPUnit regression test suite covering
/0/,/wp-content/0/, a real category slug"0", and the empty-path front page behavior.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| tests/phpunit/tests/rewrite/zeroPathSegment.php | Adds regression tests for "0" path segments and category slug "0" handling. |
| src/wp-includes/class-wp.php | Fixes rewrite-rule matching guard so "0" is treated as a real requested path. |
| src/wp-includes/class-wp-query.php | Ensures taxonomy parsing and category queried-object resolution treat "0" as a valid slug. |
Comments suppressed due to low confidence (1)
tests/phpunit/tests/rewrite/zeroPathSegment.php:31
set_category_permalink_structure()currently callsWP_UnitTestCase::set_permalink_structure(), which already flushes rewrite rules, and then flushes again aftercreate_initial_taxonomies(). That results in two rewrite rule generations per call, and the first flush happens before the%category%rewrite tag is registered.
You can avoid the extra flush by setting the permalink structure directly on $wp_rewrite and flushing once after taxonomies are registered.
$this->set_permalink_structure( $structure );
create_initial_taxonomies();
$wp_rewrite->flush_rules();
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| * `WP_Rewrite::init()` resets the registered rewrite tags to the built-in | ||
| * defaults, which do not include `%category%`. The taxonomies have to be | ||
| * re-registered and the rules flushed again, otherwise `%category%` is | ||
| * never substituted into the generated rules. |
There was a problem hiding this comment.
Nice, tightly-scoped fix — tracing this to three separately load-bearing falsy-"0" checks rather than patching just the first one you hit is the kind of thing that's easy to under-scope.
Confirmed it works: reverted the two changed src/ files against the new tests and got 2/4 failures matching the description (/0/ falls through to the front page, a real "0"-slug category 404s), then restored the patch and got all green. rewrite/query/taxonomy/canonical groups all pass, lint and PHPStan clean.
I also poked at the two things you called out as deliberately out of scope — wp_insert_term() really does discard a "0" slug (falls back to zero), and a tag forced to slug "0" still 404s post-patch — so leaving those out of this diff looks like the right call. I couldn't get a clean repro for an arbitrary custom taxonomy with a "0" slug (my test's rewrite rule never generated), so that one's untested rather than confirmed either way.
No blockers from me — not a committer, but this looks ready.
Summary
Fixes #57858.
A request path consisting of the string
0is falsy in PHP, so several truthiness guards treat it as though no path had been requested at all.With a permalink structure of
/%category%/%postname%/, a request for/0/never reaches the rewrite rule loop and silently falls through to the front page instead of resolving the(.+?)/?$catch-all rule and returning a 404.This is the case @kalpeshh re-confirmed still reproduces on trunk in comment:10 ("test case 3 ... Working but should have given 404").
Reproduction
/%category%/%postname%/example.com/0/Root cause
Three separate falsy-
"0"checks along the request path, each of which is independently load-bearing (verified by reverting them one at a time against the new tests):WP::parse_request()—if ( empty( $request_match ) )treats a requested path of"0"as an empty request, so the entire rewrite-rule loop is skipped and only the^$(home) rule is considered. This is the primary bug:matched_rulecomes back empty and no query vars are set at all.WP_Query::parse_tax_query()—! empty( $query_vars[ $t->query_var ] )skips building the taxonomy clause for a term slug of"0", sois_categoryis never set andis_homewins.handle_404()then short-circuits onis_home()and never 404s.WP_Query::get_queried_object()—elseif ( $category_name )fails to look up a category whose slug is"0", soget_queried_object()returnsnullandhandle_404()404s a category archive that genuinely exists.The change in (2) is deliberately narrow: for scalars it swaps
! empty( $x )for'' !== (string) $x, whose only behavioural delta versus the original is the numeric-zero family ("0",0).null,false,'', and empty arrays are all still skipped exactly as before.Testing
New test file
tests/phpunit/tests/rewrite/zeroPathSegment.php(4 tests):/0/404s when no matching category exists0resolves to its archive/wp-content/0/404s/still resolves to the front pageThe two guard tests pass both before and after; they exist to prove the fix does not 404 a legitimate
"0"category, and does not regress the empty-request path (which is also falsy).Full suite, single site:
No failures or errors. The 86 warnings are pre-existing PHPUnit 10 forward-compatibility notices (
Expecting E_DEPRECATED ... is deprecated) and are unrelated to this change. Focused groups--group rewrite(1392),--group query(1896),--group canonical(1057) and--group taxonomy(878) all pass.PHPCS on
class-wp-query.phpreports an identical 0 errors / 34 warnings before and after the patch; none of the warnings fall on the changed lines.A note on the test setup
WP_Rewrite::init()resets the registered rewrite tags to the built-in defaults, which do not include%category%. Tests that use a%category%permalink structure therefore have to callcreate_initial_taxonomies()and flush again, otherwise%category%is never substituted and the generated rules are literally%category%/?$ => index.php?%category%$matches[1]. This mirrors the existing approach intests/phpunit/tests/query/verboseRewriteRules.php.Out of scope (siblings found while investigating)
Two adjacent instances of the same falsy-
"0"defect turned up but are deliberately not included here, to keep the diff reviewable:wp_insert_term()discards a slug of"0"—! empty( $args['slug'] )falls back to a slug derived from the name, so a category with slug"0"cannot currently be created through the API at all. (The new test forces the slug directly via$wpdbfor this reason.)WP_Query::get_queried_object(), theis_tagbranch —elseif ( $tag )has the identical problem for a tag slug of"0". Not reachable as a reported bug today, partly because of thewp_insert_term()issue above.Happy to fold either or both in, or open separate tickets, if reviewers prefer.
The
.html-suffixed variant discussed in comment:3/comment:4 and the pagination report in comment:11 are not addressed here; the multi-segment cases from the original report (/wp-content/0/etc.) already 404 on current trunk.Trac ticket: https://core.trac.wordpress.org/ticket/57858
Use of AI Tools
AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5
Used for: Investigation, implementation, and test drafting; the root cause was confirmed empirically by tracing the request through the local Docker environment, and the final code and tests were reviewed and verified by me.