pre_handle_404filter-hookWP 4.5.0

Allows short-circuiting the standard HTTP status handling after the main query has run.

While the hook runs, the main query in the global $wp_query variable can be inspected. If necessary, you can send the request to a 404 page when it would not be a 404 by default, or prevent a 404 when WordPress would normally generate one.

If the filter returns anything other than false, all checks following this hook are skipped.

What is checked after this hook?

  • Nothing if is_404() is already true.
  • If $wp_query->posts contains posts but this is a paginated single-post request (<!--nextpage-->) and the requested pagination page has no content, a 404 page is displayed.
  • An author page for an author without posts does not produce a 404.
  • A recognized archive request does not produce a 404.
  • No 404 is produced if any of these tags are true: is_home() || is_search() || is_feed().

Usage

add_filter( 'pre_handle_404', 'wp_kama_pre_handle_404_filter', 10, 2 );

/**
 * Function for `pre_handle_404` filter-hook.
 * 
 * @param bool     $preempt  Whether to short-circuit default header status handling.
 * @param WP_Query $wp_query WordPress Query object.
 *
 * @return bool
 */
function wp_kama_pre_handle_404_filter( $preempt, $wp_query ){
	// filter...
	return $preempt;
}

Parameters

$preempt(bool)
Returning anything other than false stops WP::handle_404().
Default: false
$wp_query(object)

The global $wp_query object.

It can also be obtained inside the callback by adding global $wp_query; at the beginning.

Examples

#1 Display a 404 page for author archives

Suppose author archive pages are not needed. This may be the case when only one author publishes posts on the site. Opening /author/login displays that author's posts and also reveals the author's login. Preventing the login from being discovered can help protect against password guessing.

This example shows how to hide author archive pages. It was also discussed in this question.

// Set the 404 status
add_filter( 'pre_handle_404', 'remove_author_pages_page' );
function remove_author_pages_page( $false ) {
	if ( is_author() ) {
		global $wp_query;
		$wp_query->set_404();
		status_header( 404 );
		nocache_headers();

		return true; // Stop further processing
	}

	return $false;
}

// Remove the link
add_filter( 'author_link', 'remove_author_pages_link' );
function remove_author_pages_link( $content ) {
	return home_url();
}

#2 Set a 404 page for a custom post type entry

Suppose the realty post type uses a custom URL containing taxonomies, although the post slug alone is enough to retrieve the post. The URL looks like /realty/type_deal_taxonomy/post_slug. If any value is specified as the taxonomy term name, the post page still works. We need only the URL containing the correct taxonomy name to work; an incorrect name should return a 404 page.

## A 404 page when incorrect taxonomy names are specified in the URL.
## WordPress subsequently redirects to the correct URL with a 301 response...
add_filter( 'pre_handle_404', 'realty_404_test', 10, 2 );
function realty_404_test( $false, $wp_query ){

	$set_404 = false;

	// A single listing
	if( is_singular('realty') ){
		$post = get_queried_object();

		$term_name = get_query_var('type_deal');
		if( $term_name && ! has_term( $term_name, 'type_deal', $post ) ){
			$set_404 = 1;
		}

	}

	if( $set_404 ){
		$wp_query->set_404();
		status_header( 404 );
		nocache_headers();

		return 'stop'; // Skip the following checks
	}

	return $false; // Do nothing
}

#3 Return 404 for pages whose URL contains a space

If a static page URL contains a space, WordPress still displays the page even though it should return a 404. Such a URL returns a 200 response and creates duplicate content. For example, the real URL is /sample-page/, but it is entered in content as /sample- page/, which ultimately becomes /sample-%20page/.

add_filter( 'pre_handle_404', function ( $false, $wp_query ) {
	// The value must be taken specifically from the 'query' element!
	$pagename = $wp_query->query['pagename'] ?? '';

	// If the page slug contains a space...
	if( strpos( $pagename, '%20' ) !== false ) {

		$wp_query->set_404();
		status_header( 404 );
		nocache_headers();

		return 'stop';
	}

	return $false; // Do nothing
}, 10, 2 );

#4 Return 404 for pages whose URL contains zero

// Variant for Posts
add_filter( 'pre_handle_404', 'fix_fake_page' );

function fix_fake_page( $false ) {
	global $wp_query;

	if ( is_singular()
		 && isset( $wp_query->query['page'] )
		 && $wp_query->query['page'] === '0'
	) {
		$wp_query->set_404();
		status_header( 404 );
		nocache_headers();
		return 'stop';
	}

	return $false;
}

// Variant for Categories
add_filter( 'pre_handle_404', 'fix_fake_category_page' );

function fix_fake_category_page( $false ) {
	global $wp_query;

	if ( is_archive() && get_query_var('category_name') === '0' ) {
	// if ( is_archive() && ! term_exists( get_query_var( 'category_name' ) ) ) {
		$wp_query->set_404();
		status_header( 404 );
		nocache_headers();
		return 'stop';
	}

	return $false;
}

#5 Return 404 for post pagination pages

Post content can be split with the tag <!--nextpage-->, so each text part will be available at its own address.

For example, let’s add 2 such tags to the content, thereby splitting it into 3 parts, which will be accessible at the following addresses:

  • Part 1 — https://site.com/test-post/
  • Part 2 — https://site.com/test-post/2/
  • Part 3 — https://site.com/test-post/3/

If you go to https://site.com/test-post/1/, it will redirect to https://site.com/test-post/, everything is logical. But if you go to https://site.com/test-post/4/ or any other non-existent part of the content, then everything will be displayed as usual, but there will be no text (part 4 doesn’t exist). That means the page exists, but there is no content. In such cases an error 404 should be returned, but WP sends a 200 response, which may be bad for SEO. Let’s fix this behavior.

This drawback is not on all sites!

By default, WP redirects to the main URL with a non-existent pagination page of posts.

Most likely this is related to disabling the redirect_canonical() function:

remove_action( 'template_redirect', 'redirect_canonical' );
// Option 1. Disable pagination for individual pages
// (the trend is that no one uses them)
add_filter( 'pre_handle_404', 'remove_single_post_pagination', 10, 2 );

function remove_single_post_pagination( $false, $wp_query ) {

	if ( is_singular() && get_query_var( 'page' ) ) {

		$wp_query->set_404();
		status_header( 404 );
		nocache_headers();

		return 'stop';
	}

	return $false;
}

Changelog

Since 4.5.0 Introduced.

Where the hook is called

WP::handle_404()
pre_handle_404
wp-includes/class-wp.php 738
if ( false !== apply_filters( 'pre_handle_404', false, $wp_query ) ) {

Where the hook is used in WordPress

Usage not found.