Skip to content

Rewrite Rules: Treat a request path of "0" as a real request - #12740

Open
MicahelE wants to merge 1 commit into
WordPress:trunkfrom
MicahelE:fix/57858-zero-slug-category-archive
Open

Rewrite Rules: Treat a request path of "0" as a real request#12740
MicahelE wants to merge 1 commit into
WordPress:trunkfrom
MicahelE:fix/57858-zero-slug-category-archive

Conversation

@MicahelE

Copy link
Copy Markdown

Summary

Fixes #57858.

A request path consisting of the string 0 is 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

  1. Settings → Permalinks → Custom Structure: /%category%/%postname%/
  2. Visit example.com/0/
  3. It renders the front page (HTTP 200) instead of a 404.

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):

  1. 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_rule comes back empty and no query vars are set at all.
  2. WP_Query::parse_tax_query()! empty( $query_vars[ $t->query_var ] ) skips building the taxonomy clause for a term slug of "0", so is_category is never set and is_home wins. handle_404() then short-circuits on is_home() and never 404s.
  3. WP_Query::get_queried_object()elseif ( $category_name ) fails to look up a category whose slug is "0", so get_queried_object() returns null and handle_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):

Test Before After
/0/ 404s when no matching category exists ❌ fails (renders front page) ✅ passes
Category with slug 0 resolves to its archive ❌ fails (404s) ✅ passes
/wp-content/0/ 404s ✅ passes ✅ passes (guard)
/ still resolves to the front page ✅ passes ✅ passes (guard)

The 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).

npm run test:php -- --filter Tests_Rewrite_ZeroPathSegment
OK (4 tests, 8 assertions)

Full suite, single site:

Tests: 30520, Assertions: 4558386, Warnings: 86, Skipped: 49

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.php reports 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 call create_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 in tests/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 $wpdb for this reason.)
  • WP_Query::get_queried_object(), the is_tag branchelseif ( $tag ) has the identical problem for a tag slug of "0". Not reachable as a reported bug today, partly because of the wp_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.

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.
Copilot AI review requested due to automatic review settings July 28, 2026 17:19
@github-actions

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 micahele.

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.

Copilot AI 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.

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 using empty() which misclassifies "0".
  • Updates WP_Query::parse_tax_query() and WP_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 calls WP_UnitTestCase::set_permalink_structure(), which already flushes rewrite rules, and then flushes again after create_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.

Comment on lines +19 to +22
* `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.

@irozum irozum 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.

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.

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