Make WordPress Core

Opened 8 weeks ago

Closed 5 weeks ago

Last modified 5 weeks ago

#65945 closed defect (bug) (fixed)

Native sitemap is served with a 404 status on sites with no published posts

Reported by: fernandot Owned by: iamchitti
Priority: normal Milestone: 7.1.1
Component: Sitemaps Version: 7.1
Severity: normal Keywords: has-test-info has-patch has-unit-tests fixed-major dev-reviewed
Cc: Focuses:

Description

On a site with no published posts, /wp-sitemap.xml returns the correct sitemap index XML
but with an HTTP 404 status. Search engines discard a sitemap that does not answer 200, so
the site effectively has no sitemap while looking perfectly fine in a browser.

This affects any site without a blog: shops, LMS, brochure sites, portfolios. It is not
caused by plugins or by the server. It reproduces on a clean install with no plugins.

Steps to reproduce

  1. Install WordPress and set any permalink structure other than plain.
  2. Delete the sample "Hello world!" post, so the site has zero published posts of type post. Pages may still exist, it makes no difference.
  3. Request /wp-sitemap.xml.

Expected: HTTP 200 with the sitemap index.

Actual: HTTP 404 with the sitemap index. The body is correct and complete.

Publishing a single post of type post makes the same URL return 200 again. Deleting it
brings the 404 back.

Verified on a clean install with no plugins, with both show_on_front=posts and
show_on_front=page:

home        posts   pages   /wp-sitemap.xml
blog             1       1       200
blog             0       1       404
static page      1       1       200
static page      0       1       404

/?sitemap=index behaves the same way, so this is not about rewrite rules. On a healthy
site that URL 301s to the pretty permalink; on an affected site it renders the XML directly
under the 404.

/wp-sitemap-index.xsl is not affected and returns 200, because that branch of
WP_Sitemaps::render_sitemaps() exits before anything else.

Why it happens

WP::handle_404() runs during WP::main(), before template_redirect. For a sitemap
request the main query is the default query, and none of the conditionals are set. Dumped
from inside pre_handle_404 on a clean install:

no posts:  is_404=false is_home=false is_singular=false is_archive=false is_feed=false is_paged=false posts=0  -> 404
one post:  is_404=false is_home=false is_singular=false is_archive=false is_feed=false is_paged=false posts=1  -> 200

Every exception in handle_404() is false, including is_home(), so the only thing
standing between a sitemap request and a 404 is whether $wp_query->posts happens to be
non-empty. That is incidental: the posts in that query have nothing to do with the sitemap
being rendered.

WP_Sitemaps::render_sitemaps() then runs on template_redirect, renders the XML and
exits without ever calling status_header( 200 ) or clearing is_404, so the status set
earlier stands.

Precedent

#39157 reported exactly this for feeds ("Feed returns 404 when there are no posts") and was
fixed in 4.7.3. The same reasoning applies here: an empty result set is not a missing
resource, and a machine-readable endpoint that renders its content should not answer 404.

Possible fixes

Either of these works. A one-line drop-in confirmed on a clean install and on a live site:

<?php
add_filter( 'pre_handle_404', function ( $preempt, $wp_query ) {
        if ( ! get_query_var( 'sitemap' ) && ! get_query_var( 'sitemap-stylesheet' ) ) {
                return $preempt;
        }
        $wp_query->is_404 = false;
        status_header( 200 );
        return true;
}, 10, 2 );
  1. Add sitemap requests to the exceptions in WP::handle_404(), next to is_feed().
  2. Or have WP_Sitemaps::render_sitemaps() set its own status, clearing is_404 and sending a 200 before rendering, which keeps the fix inside the sitemaps component.

Note that the 404 render_sitemaps() forces when a provider returns no URLs must stay: with
the filter above, /wp-sitemap-posts-post-1.xml on a site with no posts still correctly
returns 404. That case is #61293 and is a separate discussion.

  • #39157 - same bug for feeds, fixed in 4.7.3
  • #61293 - empty sitemap vs 404, different cause, has-patch
  • #51004 - sitemaps 404 on subsites of a subdirectory multisite

Change History (23)

#1 follow-up: @audrasjb
8 weeks ago

  • Milestone Awaiting Review → 7.2
  • Version 7.1

Removing version 7.1 as the issue wasn't introduced in 7.1, and moving to milestone 7.2 as it sounds like something we can fix in short term.

#2 in reply to: ↑ 1 @siliconforks
8 weeks ago

Replying to audrasjb:

Removing version 7.1 as the issue wasn't introduced in 7.1, and moving to milestone 7.2 as it sounds like something we can fix in short term.

I think this issue was introduced in 7.1? There are a bunch of people reporting this (or something very similar to this) in just the last few days since 7.1 was released.

See:

#65936
#65941
#65945 (this issue)

#3 @iamchitti
8 weeks ago

This is reproducible on a clean install. The cause looks to be in WP::handle_404() rather than in the sitemaps component: sitemap requests aren't in its list of exceptions, so an empty main query is enough to 404 them.

One note on the version question: there appear to be two distinct cases here, one that changed in 7.1 and one that predates it, so the exception may need to go somewhere other than next to is_feed() as suggested above. Will raise a fix PR after confirming a few details in codebase.

This ticket was mentioned in ​PR #13247 on ​WordPress/wordpress-develop by ​@iamchitti.


8 weeks ago
#4

  • Keywords has-patch has-unit-tests added; needs-patch removed

## Ticket

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

## Summary

  • On a site with zero published posts, /wp-sitemap.xml renders correct XML but responds with HTTP 404, so search engines discard it.
  • ​`WP::handle_404()` 404s the main query when it has no posts and no exception applies. Sitemap requests aren't in that list — they were only shielded by falling through to ​`is_home = true`, which changeset 62664 (#51542, #51543) correctly removed.
  • Same class of issue as #39157, fixed for feeds in 4.7.3.

## Changes

  1. WP::handle_404() — exempt sitemaps alongside the other machine-readable endpoints. Placed in the first branch rather than next to is_feed(), since provider routes map paged from the URL and so never reach the elseif ( ! is_paged() ) branch.
  2. WP_Sitemaps::render_sitemaps() — 404 explicitly for an unregistered provider, which previously relied on handle_404() doing it.

Intentional 404s are unaffected — sitemaps disabled and empty URL lists (#61293) call set_404() directly, later in the request.

## Test

New tests/phpunit/tests/wp/handle404.php (12 tests): index, provider, paged, taxonomy, user and stylesheet routes with no posts, plus guards that unknown URLs and unregistered providers still 404.

  • --group sitemaps
  • --group wp --group query --group feed --group canonical --group rewrite

## Screenshots

NA

## Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5
Used for: Root-cause investigation, tracing history, and drafting the fix and tests.

#5 @wildworks
8 weeks ago

  • Milestone 7.2 → 7.1.1

I think this issue was introduced in 7.1? There are a bunch of people reporting this (or something very similar to this) in just the last few days since 7.1 was released.

See:

#65936
#65941
#65945 (this issue)

If this is the case, it should be fixed in 7.1.1. First, let's clarify which version this issue occurred in and why.

#6 follow-up: @wildworks
8 weeks ago

Based on my findings, this issue occurred in r62664, so I believe it should be fixed in a minor release.

cc @masteradhoc @westonruter

This ticket was mentioned in ​Slack in #core by adrianduffell. ​View the logs.


6 weeks ago

#8 in reply to: ↑ 6 @harishtewari
6 weeks ago

Replying to wildworks:

Based on my findings, this issue occurred in r62664, so I believe it should be fixed in a minor release.

cc @masteradhoc @westonruter

I agree with @wildworks

#9 @westonruter
6 weeks ago

  • Owner set to iamchitti
  • Status new → assigned

​@adamsilverstein commented on ​PR #13247:


6 weeks ago
#10

Please note the RC for 7.1.1 is September 10th if we want it included in that version. Otherwise it is fine to punt to 7.1.2.

This ticket was mentioned in ​Slack in #core by adamsilverstein. ​View the logs.


6 weeks ago

#12 @adamsilverstein
6 weeks ago

#65936 was marked as a duplicate.

This ticket was mentioned in ​Slack in #core by adrianduffell. ​View the logs.


6 weeks ago

#14 @westonruter
5 weeks ago

  • Version → 7.1

​@westonruter commented on ​PR #13247:


5 weeks ago
#15

Sorry for the back-and-forth on this. I think for 7.1.1 we'll go back to just the theme's 404 template as it is a smaller change with less risk, and then in 7.2 we can improve the error handling with wider testing. I'm going to have Claude share some review feedback that led me to this.

#16 @westonruter
5 weeks ago

  • Resolution → fixed
  • Status assigned → closed

In 63570:

Sitemaps: Don't 404 valid sitemaps on sites with no posts.

WP::handle_404() sets a 404 when the main query matches no posts and no exception applies. Sitemap requests were never among those exceptions; they were shielded only incidentally, by falling through to is_home, and in r62664 that fallthrough was removed. A site with no published posts therefore served a complete, valid sitemap under a 404 status, which search engines discard.

Exempt sitemap and stylesheet routes there, alongside the existing admin, robots and favicon exceptions. Since handle_404() no longer decides the status for these requests, every sitemap 404 now has to be issued by WP_Sitemaps::render_sitemaps() instead: an unregistered provider, an unrecognized stylesheet type, and a route whose query vars do not survive sanitize_text_field() would each otherwise be served as a 200 on an arbitrary URL. These share a send_404() helper, which also sends the no-cache headers handle_404() was previously contributing, so an intermediary does not retain a 404 for a route that becomes valid once the site has more content.

Sitemaps disabled via the wp_sitemaps_enabled filter, and providers with an empty URL list, keep the status they already had; whether the latter should render an empty sitemap instead is #61293.

Developed in ​https://github.com/WordPress/wordpress-develop/pull/13247.
Follow-up to r48072, r48523, r62664.

Props iamchitti, westonruter, fernandot, wildworks, harishtewari, l1onofjudah, luksusspokoju, abrahamfariaz, andreasca, siliconforks, adamsilverstein, audrasjb, ocean90, mrkenobi.
See #39157, #61293.
Fixes #65945.

#17 @westonruter
5 weeks ago

  • Keywords dev-feedback fixed-major added
  • Resolution fixed
  • Status closed → reopened

Re-opening for 7.1.1 consideration.

#18 @adamsilverstein
5 weeks ago

  • Keywords dev-reviewed added; dev-feedback removed

+1 for landing in 7.1 @westonruter - looks good, thanks!

#19 @westonruter
5 weeks ago

  • Resolution → fixed
  • Status reopened → closed

In 63573:

Sitemaps: Don't 404 valid sitemaps on sites with no posts.

WP::handle_404() sets a 404 when the main query matches no posts and no exception applies. Sitemap requests were never among those exceptions; they were shielded only incidentally, by falling through to is_home, and in r62664 that fallthrough was removed. A site with no published posts therefore served a complete, valid sitemap under a 404 status, which search engines discard.

Exempt sitemap and stylesheet routes there, alongside the existing admin, robots and favicon exceptions. Since handle_404() no longer decides the status for these requests, every sitemap 404 now has to be issued by WP_Sitemaps::render_sitemaps() instead: an unregistered provider, an unrecognized stylesheet type, and a route whose query vars do not survive sanitize_text_field() would each otherwise be served as a 200 on an arbitrary URL. These share a send_404() helper, which also sends the no-cache headers handle_404() was previously contributing, so an intermediary does not retain a 404 for a route that becomes valid once the site has more content.

Sitemaps disabled via the wp_sitemaps_enabled filter, and providers with an empty URL list, keep the status they already had; whether the latter should render an empty sitemap instead is #61293.

Developed in ​https://github.com/WordPress/wordpress-develop/pull/13247.
Follow-up to r48072, r48523, r62664.

Reviewed by adamsilverstein.
Merges r63570 to the 7.0 branch.

Props iamchitti, westonruter, fernandot, wildworks, harishtewari, l1onofjudah, luksusspokoju, abrahamfariaz, andreasca, siliconforks, adamsilverstein, audrasjb, ocean90, mrkenobi.
See #39157, #61293.
Fixes #65945.

#20 @westonruter
5 weeks ago

  • Resolution fixed
  • Status closed → reopened

I committed to the 7.0 branch when I meant the 7.1 branch. Fixing.

#21 @westonruter
5 weeks ago

In 63574:

Sitemaps: Revert r63573 from the 7.0 branch.

The sitemap 404 fix merged in r63573 was committed to the 7.0 branch in error; it was intended for the 7.1 branch. This restores the branch to its state as of r63449 and removes the recorded merge of r63570, leaving 7.0 unchanged from what shipped in 7.0.4.

Reverts r63573.

See #65945.

#22 @westonruter
5 weeks ago

  • Resolution → fixed
  • Status reopened → closed

In 63575:

Sitemaps: Don't 404 valid sitemaps on sites with no posts.

WP::handle_404() sets a 404 when the main query matches no posts and no exception applies. Sitemap requests were never among those exceptions; they were shielded only incidentally, by falling through to is_home, and in r62664 that fallthrough was removed. A site with no published posts therefore served a complete, valid sitemap under a 404 status, which search engines discard.

Exempt sitemap and stylesheet routes there, alongside the existing admin, robots and favicon exceptions. Since handle_404() no longer decides the status for these requests, every sitemap 404 now has to be issued by WP_Sitemaps::render_sitemaps() instead: an unregistered provider, an unrecognized stylesheet type, and a route whose query vars do not survive sanitize_text_field() would each otherwise be served as a 200 on an arbitrary URL. These share a send_404() helper, which also sends the no-cache headers handle_404() was previously contributing, so an intermediary does not retain a 404 for a route that becomes valid once the site has more content.

Sitemaps disabled via the wp_sitemaps_enabled filter, and providers with an empty URL list, keep the status they already had; whether the latter should render an empty sitemap instead is #61293.

Developed in ​https://github.com/WordPress/wordpress-develop/pull/13247.
Follow-up to r48072, r48523, r62664.

Reviewed by adamsilverstein.
Merges r63570 to the 7.1 branch.
Merged to the 7.0 branch in error in r63573 and reverted in r63574.

Props iamchitti, westonruter, fernandot, wildworks, harishtewari, l1onofjudah, luksusspokoju, abrahamfariaz, andreasca, siliconforks, adamsilverstein, audrasjb, ocean90, mrkenobi.
See #39157, #61293.
Fixes #65945.

#23 @westonruter
5 weeks ago

#66083 was marked as a duplicate.

Note: See TracTickets for help on using tickets.